diff --git a/.editorconfig b/.editorconfig index 5e54457699..872fe6c2ca 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,7 +14,7 @@ indent_size = 4 max_line_length = 100 [*.py] -indent_size = 2 +indent_size = 4 [*.gradle] indent_size = 4 diff --git a/.github/workflows/ai-engine.yml b/.github/workflows/ai-engine.yml new file mode 100644 index 0000000000..ed0569c312 --- /dev/null +++ b/.github/workflows/ai-engine.yml @@ -0,0 +1,83 @@ +name: AI Engine CI + +on: + push: + branches: [main] + pull_request: + +jobs: + engine: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + defaults: + run: + working-directory: engine + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + + - name: Install dependencies + run: make install + + - name: Run fixers + # Ignore errors here because we're going to add comments for them in the following steps before actually failing + run: make fix || true + + - name: Check for fixer changes + id: fixer_changes + run: | + if git diff --quiet; then + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Post fixer suggestions + if: steps.fixer_changes.outputs.changed == 'true' && github.event_name == 'pull_request' + uses: reviewdog/action-suggester@v1 + continue-on-error: true + with: + tool_name: engine-make-fix + github_token: ${{ secrets.GITHUB_TOKEN }} + filter_mode: file + fail_level: any + level: info + + - name: Comment on fixer suggestions + if: steps.fixer_changes.outputs.changed == 'true' && github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: "The Python code in your PR has formatting/linting issues. Consider running `make fix` locally or setting up your editor's Ruff integration to auto-format and lint your files as you go, or commit the suggested changes on this PR.", + }); + + - name: Verify fixer changes are committed + if: steps.fixer_changes.outputs.changed == 'true' + run: | + if ! git diff --exit-code; then + echo "Fixes are out of date." + echo "Apply the reviewdog suggestions or run 'make fix' from engine/ and commit the updated files." + git --no-pager diff --stat + exit 1 + fi + + - name: Run linting + run: make lint + + - name: Run type checking + run: make typecheck + + - name: Run tests + run: make test diff --git a/AGENTS.md b/AGENTS.md index 99e563c963..d67040db64 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,6 +19,18 @@ This file provides guidance to AI Agents when working with code in this reposito ### Security Mode Development Set `DOCKER_ENABLE_SECURITY=true` environment variable to enable security features during development. This is required for testing the full version locally. +### Python Development +Development for the AI engine happens in the `engine/` folder. It's built with Langchain and Pydantic and allows for the creation and editing of PDF documents. The frontend calls the Python via Java as a proxy. + +- Python version is 3.13; use modern Python features (type aliases, pattern matching, dataclasses, etc.) where they help clarity. +- Write fully type-correct code; keep pyright clean and avoid `Any` unless strictly necessary. +- JSON handling: deserialize into fully typed Pydantic models as early as possible, and serialize back from Pydantic models as late as possible. +- Use Makefile commands for Python work: + - From `engine/`: `make check` to lint, type-check, test, etc. and `make fix` to fix easily fixable linting & formatting issues. +- The project structure is defined in `engine/pyproject.toml`. Any new dependencies should be listed appropriately there, followed by running `make install`. +- Prefer using classes to nesting functions, and make other similar architectural decisions to improve testability. Do not nest classes or functions unless specifically required to for the code construct (like a decorator). +- All environment variables used within the code must begin with the `STIRLING_` prefix in order to keep them unique and easier to find. + ### Frontend Development - **Frontend dev server**: `cd frontend && npm run dev` (requires backend on localhost:8080) - **Tech Stack**: Vite + React + TypeScript + Mantine UI + TailwindCSS @@ -82,7 +94,7 @@ export function RightRailFooterExtensions(_props: RightRailFooterExtensionsProps } ``` -```typescript +```tsx // desktop/components/rightRail/RightRailFooterExtensions.tsx (real implementation) import { Box } from '@mantine/core'; import { BackendHealthIndicator } from '@app/components/BackendHealthIndicator'; @@ -100,7 +112,7 @@ export function RightRailFooterExtensions({ className }: RightRailFooterExtensio } ``` -```typescript +```tsx // core/components/shared/RightRail.tsx (usage - works in ALL builds) import { RightRailFooterExtensions } from '@app/components/rightRail/RightRailFooterExtensions'; diff --git a/LICENSE b/LICENSE index c572f474d7..e7a8034e41 100644 --- a/LICENSE +++ b/LICENSE @@ -6,6 +6,8 @@ Portions of this software are licensed as follows: * All content that resides under the "app/proprietary/" directory of this repository, if that directory exists, is licensed under the license defined in "app/proprietary/LICENSE". +* All content that resides under the "engine/" directory of this repository, +if that directory exists, is licensed under the license defined in "engine/LICENSE". * All content that resides under the "frontend/src/proprietary/" directory of this repository, if that directory exists, is licensed under the license defined in "frontend/src/proprietary/LICENSE". * All content that resides under the "frontend/src/desktop/" directory of this repository, diff --git a/engine/.gitignore b/engine/.gitignore new file mode 100644 index 0000000000..1116f48815 --- /dev/null +++ b/engine/.gitignore @@ -0,0 +1,46 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +ENV/ +build/ +dist/ +*.egg-info/ + +# Node +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.vite/ + +# Environment +.env +.env.local + +# LaTeX outputs +*.aux +*.log +*.out +*.toc +*.pdf +*.tex +!src/default_templates/*.tex +data/ +output/ +logs/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db diff --git a/engine/.python-version b/engine/.python-version new file mode 100644 index 0000000000..86105ead5f --- /dev/null +++ b/engine/.python-version @@ -0,0 +1 @@ +3.13.8 diff --git a/engine/Dockerfile b/engine/Dockerfile new file mode 100644 index 0000000000..e419c380c3 --- /dev/null +++ b/engine/Dockerfile @@ -0,0 +1,63 @@ +# syntax=docker/dockerfile:1.5 +FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim + +# Install system deps: Node.js + Puppeteer/Chromium runtime libraries +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + ca-certificates \ + poppler-utils \ + nodejs \ + npm \ + # Fonts for correct text rendering in generated PDFs + fonts-liberation \ + fonts-dejavu-core \ + # Chromium headless runtime deps + libnss3 \ + libatk1.0-0 \ + libatk-bridge2.0-0 \ + libcups2 \ + libxkbcommon0 \ + libxcomposite1 \ + libxdamage1 \ + libxrandr2 \ + libpango-1.0-0 \ + libcairo2 \ + libasound2 \ + libx11-6 \ + libxext6 \ + libxfixes3 \ + libgbm1 \ + libdrm2 \ + libxshmfence1 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Install Node dependencies (Puppeteer) first for layer caching +COPY package.json package-lock.json ./ +RUN npm ci + +# Install Python dependencies +COPY pyproject.toml uv.lock ./ +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --frozen --no-dev + +# Copy Python source into /app/src/ +COPY src/ ./src/ + +# Create output directories +RUN mkdir -p /app/output /app/data + +# Expose port +EXPOSE 5001 + +# Environment variables for Gunicorn configuration +ENV GUNICORN_WORKERS=4 +ENV GUNICORN_TIMEOUT=120 +# Disable Python stdout/stderr buffering so log output appears immediately in Docker logs +ENV PYTHONUNBUFFERED=1 + +ENV PATH="/app/.venv/bin:$PATH" +WORKDIR /app/src +CMD ["sh", "-c", "gunicorn --bind 0.0.0.0:5001 --workers ${GUNICORN_WORKERS} --timeout ${GUNICORN_TIMEOUT} --worker-class gthread --threads 4 app:app"] diff --git a/engine/Dockerfile.dev b/engine/Dockerfile.dev new file mode 100644 index 0000000000..2c3649d233 --- /dev/null +++ b/engine/Dockerfile.dev @@ -0,0 +1,59 @@ +# syntax=docker/dockerfile:1.5 +FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim + +# Install system deps: Node.js + Puppeteer/Chromium runtime libraries +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + ca-certificates \ + poppler-utils \ + nodejs \ + npm \ + # Fonts for correct text rendering in generated PDFs + fonts-liberation \ + fonts-dejavu-core \ + # Chromium headless runtime deps + libnss3 \ + libatk1.0-0 \ + libatk-bridge2.0-0 \ + libcups2 \ + libxkbcommon0 \ + libxcomposite1 \ + libxdamage1 \ + libxrandr2 \ + libpango-1.0-0 \ + libcairo2 \ + libasound2 \ + libx11-6 \ + libxext6 \ + libxfixes3 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Install Node dependencies (Puppeteer) first for layer caching +COPY package.json package-lock.json ./ +RUN npm ci + +# Copy only pyproject.toml and uv.lock first for dependency caching +COPY pyproject.toml uv.lock ./ + +# Install dependencies including dev extras +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --frozen + +# Create output directories +RUN mkdir -p /app/output /app/data + +# Expose port +EXPOSE 5001 + +# Set environment for Flask development +ENV FLASK_ENV=development +ENV FLASK_DEBUG=1 +ENV PYTHONUNBUFFERED=1 + +# Run the Flask app with hot reload +# Note: src/ will be mounted as a volume at runtime +WORKDIR /app/src +CMD ["uv", "run", "python", "app.py"] diff --git a/engine/LICENSE b/engine/LICENSE new file mode 100644 index 0000000000..d268556808 --- /dev/null +++ b/engine/LICENSE @@ -0,0 +1,51 @@ +Stirling PDF User License + +Copyright (c) 2025 Stirling PDF Inc. + +License Scope & Usage Rights + +Production use of the Stirling PDF Software is only permitted with a valid Stirling PDF User License. + +For purposes of this license, “the Software” refers to the Stirling PDF application and any associated documentation files +provided by Stirling PDF Inc. You or your organization may not use the Software in production, at scale, or for business-critical +processes unless you have agreed to, and remain in compliance with, the Stirling PDF Subscription Terms of Service +(https://www.stirlingpdf.com/terms) or another valid agreement with Stirling PDF, and hold an active User License subscription +covering the appropriate number of licensed users. + +Trial and Minimal Use + +You may use the Software without a paid subscription for the sole purposes of internal trial, evaluation, or minimal use, provided that: +* Use is limited to the capabilities and restrictions defined by the Software itself; +* You do not copy, distribute, sublicense, reverse-engineer, or use the Software in client-facing or commercial contexts. + +Continued use beyond this scope requires a valid Stirling PDF User License. + +Modifications and Derivative Works + +You may modify the Software only for development or internal testing purposes. Any such modifications or derivative works: + +* May not be deployed in production environments without a valid User License; +* May not be distributed or sublicensed; +* Remain the intellectual property of Stirling PDF and/or its licensors; +* May only be used, copied, or exploited in accordance with the terms of a valid Stirling PDF User License subscription. + +Prohibited Actions + +Unless explicitly permitted by a paid license or separate agreement, you may not: + +* Use the Software in production environments; +* Copy, merge, distribute, sublicense, or sell the Software; +* Remove or alter any licensing or copyright notices; +* Circumvent access restrictions or licensing requirements. + +Third-Party Components + +The Stirling PDF Software may include components subject to separate open source licenses. Such components remain governed by +their original license terms as provided by their respective owners. + +Disclaimer + +THE SOFTWARE IS PROVIDED “AS IS,” WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/engine/Makefile b/engine/Makefile new file mode 100644 index 0000000000..64f91131b9 --- /dev/null +++ b/engine/Makefile @@ -0,0 +1,87 @@ +.PHONY: help install prep check fix lint lint-fix lint-fix-unsafe format format-check typecheck test run run-dev clean tool-models docker-build docker-run + +REPO_ROOT_DIR = .. +ROOT_DIR = . +VENV_DIR = $(ROOT_DIR)/.venv +DEPS_STAMP := $(VENV_DIR)/.deps-installed +NODE_DEPS_STAMP := node_modules/.deps-installed +TOOL_MODELS := $(CURDIR)/src/models/tool_models.py +FRONTEND_DIR := $(REPO_ROOT_DIR)/frontend +FRONTEND_TSX := $(FRONTEND_DIR)/node_modules/.bin/tsx + +$(DEPS_STAMP): $(ROOT_DIR)/uv.lock $(ROOT_DIR)/pyproject.toml + uv python install 3.13.8 + uv sync + touch $(DEPS_STAMP) + +$(NODE_DEPS_STAMP): package-lock.json + npm ci + touch $(NODE_DEPS_STAMP) + +help: + @echo "Engine commands:" + @echo " make install - Install production dependencies" + @echo " make prep - Set up .env file from .env.example" + @echo " make lint - Run linting checks" + @echo " make format - Format code" + @echo " make format-check - Show whether code is correctly formatted" + @echo " make typecheck - Run type checking" + @echo " make test - Run tests" + @echo " make run - Run the Flask backend with gunicorn (production)" + @echo " make run-dev - Run the Flask backend with dev server (development only)" + @echo " make tool-models - Generate src/models/tool_models_*.py from frontend TypeScript tool defs" + @echo " make clean - Clean up generated files" + @echo " make docker-build - Build Docker image" + @echo " make docker-run - Run Docker container" + +install: $(DEPS_STAMP) $(NODE_DEPS_STAMP) + +prep: install + uv run scripts/setup_env.py + +check: typecheck lint format-check test + +fix: lint-fix format + +lint: install + uv run ruff check . + +lint-fix: install + uv run ruff check . --fix + +lint-fix-unsafe: install + uv run ruff check . --fix --unsafe-fixes + +format: install + uv run ruff format . + +format-check: install + uv run ruff format . --diff + +typecheck: install + uv run pyright . --warnings + +test: prep + uv run pytest tests + +run: prep + cd src && PYTHONUNBUFFERED=1 uv run gunicorn --bind 0.0.0.0:5001 --workers 4 --timeout 120 --worker-class gthread --threads 4 app:app + +run-dev: prep + cd src && PYTHONUNBUFFERED=1 uv run python app.py + +frontend-deps: + if [ ! -x "$(FRONTEND_TSX)" ]; then cd $(FRONTEND_DIR) && npm install; fi + +tool-models: install frontend-deps + uv run python scripts/generate_tool_models.py --output $(TOOL_MODELS) + $(MAKE) fix + +clean: + rm -rf $(VENV_DIR) data logs output + +docker-build: + docker build -t stirling-pdf-engine . + +docker-run: + docker run -p 5001:5001 stirling-pdf-engine diff --git a/engine/config/.env.example b/engine/config/.env.example new file mode 100644 index 0000000000..c898254e66 --- /dev/null +++ b/engine/config/.env.example @@ -0,0 +1,51 @@ +# Claude API (Anthropic) +STIRLING_ANTHROPIC_API_KEY= + +# OpenAI API (Optional - for GPT models) +STIRLING_OPENAI_API_KEY= +STIRLING_OPENAI_BASE_URL= + +# Model Configuration +STIRLING_SMART_MODEL=claude-sonnet-4-5-20250929 +STIRLING_FAST_MODEL=claude-haiku-4-5-20251001 + +# GPT-5 Reasoning Settings (only used if SMART_MODEL is gpt-5) +# Options: minimal, low, medium, high, xhigh +STIRLING_SMART_MODEL_REASONING_EFFORT=medium +STIRLING_FAST_MODEL_REASONING_EFFORT=minimal + +# GPT-5 Text Verbosity (only used if SMART_MODEL is gpt-5) +# Options: minimal, low, medium, high +STIRLING_SMART_MODEL_TEXT_VERBOSITY=medium +STIRLING_FAST_MODEL_TEXT_VERBOSITY=low + +# Output token limits. STIRLING_AI_MAX_TOKENS is a global override (leave empty to use per-model defaults) +STIRLING_AI_MAX_TOKENS= +STIRLING_SMART_MODEL_MAX_TOKENS=8192 +STIRLING_FAST_MODEL_MAX_TOKENS=2048 +STIRLING_CLAUDE_MAX_TOKENS=4096 +STIRLING_DEFAULT_MODEL_MAX_TOKENS=4096 + +# PostHog Analytics +STIRLING_POSTHOG_API_KEY= +STIRLING_POSTHOG_HOST=https://eu.i.posthog.com + +# Backend Configuration +STIRLING_JAVA_BACKEND_URL=http://localhost:8080 +STIRLING_JAVA_BACKEND_API_KEY=123456789 +STIRLING_JAVA_REQUEST_TIMEOUT_SECONDS=30 + +# Logging and Debugging +STIRLING_AI_RAW_DEBUG=0 +STIRLING_FLASK_DEBUG=0 +# Override the log file directory (defaults to ./logs in server mode, OS log dir in desktop mode) +STIRLING_LOG_PATH= +# Enable verbose table detection debug logging in the PDF text editor +STIRLING_PDF_EDITOR_TABLE_DEBUG=0 +# Set to true when running as a Tauri desktop app (switches to OS-native log directory) +STIRLING_PDF_TAURI_MODE=false + +# AI Settings +STIRLING_AI_STREAMING=true +STIRLING_AI_PREVIEW_MAX_INFLIGHT=3 +STIRLING_AI_REQUEST_TIMEOUT=70 diff --git a/engine/package-lock.json b/engine/package-lock.json new file mode 100644 index 0000000000..8decf3bba9 --- /dev/null +++ b/engine/package-lock.json @@ -0,0 +1,1138 @@ +{ + "name": "stirling-docgen", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "stirling-docgen", + "version": "1.0.0", + "hasInstallScript": true, + "dependencies": { + "puppeteer": "^24.25.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", + "integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.2.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", + "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/b4a": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.5.tgz", + "integrity": "sha512-iEsKNwDh1wiWTps1/hdkNdmBgDlDVZP5U57ZVOlt+dNFqpc/lpPouCIxZw+DYBgc4P9NDfIZMPNR4CHNhzwLIA==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.4.tgz", + "integrity": "sha512-POK4oplfA7P7gqvetNmCs4CNtm9fNsx+IAh7jH7GgU0OJdge2rso0R20TNWVq6VoWcCvsTdlNDaleLHGaKx8CA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz", + "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.8.0.tgz", + "integrity": "sha512-reUN0M2sHRqCdG4lUK3Fw8w98eeUIZHL5c3H7Mbhk2yVBL+oofgaIp0ieLfD5QXwPCypBpmEEKU2WZKzbAk8GA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.21.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", + "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/basic-ftp": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", + "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chromium-bidi": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", + "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1566079", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1566079.tgz", + "integrity": "sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ==", + "license": "BSD-3-Clause" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/puppeteer": { + "version": "24.37.4", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.37.4.tgz", + "integrity": "sha512-SMSq+FL3gnglolhrIks3maRkrdQEjoDCesy6FXziMPWsF1DxoX+GVxRa82y+euzkzS52/UujM/BoaFPQ+AnPXQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1566079", + "puppeteer-core": "24.37.4", + "typed-query-selector": "^2.12.0" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "24.37.4", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.37.4.tgz", + "integrity": "sha512-sQYtYgaNaLYO82k2FHmr7bR1tCmo2fBupEI7Kd0WpBlMropNcfxSTLOJXVRkhiHig0dUiMI7g0yq+HJI1IDCzg==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1566079", + "typed-query-selector": "^2.12.0", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.19.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "optional": true, + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT", + "optional": true + }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", + "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/engine/package.json b/engine/package.json new file mode 100644 index 0000000000..04976d5df9 --- /dev/null +++ b/engine/package.json @@ -0,0 +1,13 @@ +{ + "name": "stirling-docgen", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Node tooling for Stirling PDF docgen backend (HTML→PDF via Puppeteer)", + "scripts": { + "postinstall": "node -e \"require('puppeteer')\" 2>/dev/null || true" + }, + "dependencies": { + "puppeteer": "^24.25.0" + } +} diff --git a/engine/pyproject.toml b/engine/pyproject.toml new file mode 100644 index 0000000000..7688b67522 --- /dev/null +++ b/engine/pyproject.toml @@ -0,0 +1,69 @@ +[project] +name = "engine" +version = "0.1.0" +description = "AI Document Engine" +requires-python = ">=3.13" +dependencies = [ + "flask>=3.0.0", + "flask-cors>=4.0.0", + "gunicorn>=25.0.3", + "openai>=2.20.0", + "langchain-core>=1.2.11", + "langchain-openai>=1.1.9", + "langchain-anthropic>=1.1.9", + "platformdirs>=4.5.1", + "pypdf>=6.7.0", + "pydantic>=2.0.0", + "posthog>=7.8.6", + "python-dotenv>=1.2.1", + "beautifulsoup4>=4.12.0", +] + +[dependency-groups] +dev = [ + "pytest>=8.0.0", + "pytest-cov>=4.1.0", + "ruff>=0.14.10", + "pyright>=1.1.408", + "datamodel-code-generator[ruff]>=0.27.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src"] +exclude = [ + "tests", +] + +[tool.ruff] +line-length = 120 +target-version = "py313" + +[tool.ruff.lint] +select = [ + "E", + "F", + "I", + "N", + "W", + "RUF100", + "UP", +] +ignore = [ + "E501", # Temporarily disable line length limit until codebase conformat +] + +[tool.pyright] +pythonVersion = "3.13" + +reportImportCycles = "warning" +reportUnnecessaryCast = "warning" +reportUnusedImport = "warning" +#reportUnknownParameterType = "warning" + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["src"] diff --git a/engine/scripts/generate_tool_models.py b/engine/scripts/generate_tool_models.py new file mode 100644 index 0000000000..abefeb5307 --- /dev/null +++ b/engine/scripts/generate_tool_models.py @@ -0,0 +1,510 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +import keyword +import re +import subprocess +from collections.abc import Callable +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +TOOL_MODELS_HEADER = """# AUTO-GENERATED FILE. DO NOT EDIT. +# Generated by scripts/generate_tool_models.py from frontend TypeScript sources. +# ruff: noqa: N815 +""" + + +OPERATION_TYPE_RE = re.compile(r"operationType\s*:\s*['\"]([A-Za-z0-9_]+)['\"]") +DEFAULT_REF_RE = re.compile(r"defaultParameters\s*:\s*([A-Za-z0-9_]+)") +DEFAULT_SHORTHAND_RE = re.compile(r"\bdefaultParameters\b") +IMPORT_RE = re.compile(r"import\s*\{([^}]+)\}\s*from\s*['\"]([^'\"]+)['\"]") +VAR_OBJ_RE_TEMPLATE = r"(?:export\s+)?const\s+{name}\b[^=]*=\s*\{{" + + +@dataclass +class ToolModelSpec: + tool_id: str + params: dict[str, Any] + param_types: dict[str, Any] + + +class ParseError(Exception): + pass + + +def _find_matching(text: str, start: int, open_char: str, close_char: str) -> int: + depth = 0 + i = start + in_str: str | None = None + while i < len(text): + ch = text[i] + if in_str: + if ch == "\\": + i += 2 + continue + if ch == in_str: + in_str = None + i += 1 + continue + if ch in {"'", '"'}: + in_str = ch + elif ch == open_char: + depth += 1 + elif ch == close_char: + depth -= 1 + if depth == 0: + return i + i += 1 + raise ParseError(f"Unmatched {open_char}{close_char} block") + + +def _extract_block(text: str, pattern: str) -> str | None: + match = re.search(pattern, text) + if not match: + return None + brace_start = text.find("{", match.end() - 1) + if brace_start == -1: + return None + brace_end = _find_matching(text, brace_start, "{", "}") + return text[brace_start : brace_end + 1] + + +def _split_top_level_items(obj_body: str) -> list[str]: + items: list[str] = [] + depth_obj = depth_arr = 0 + in_str: str | None = None + token_start = 0 + i = 0 + while i < len(obj_body): + ch = obj_body[i] + if in_str: + if ch == "\\": + i += 2 + continue + if ch == in_str: + in_str = None + i += 1 + continue + if ch in {"'", '"'}: + in_str = ch + elif ch == "{": + depth_obj += 1 + elif ch == "}": + depth_obj -= 1 + elif ch == "[": + depth_arr += 1 + elif ch == "]": + depth_arr -= 1 + elif ch == "," and depth_obj == 0 and depth_arr == 0: + piece = obj_body[token_start:i].strip() + if piece: + items.append(piece) + token_start = i + 1 + i += 1 + tail = obj_body[token_start:].strip() + if tail: + items.append(tail) + return items + + +def _resolve_import_path(repo_root: Path, current_file: Path, module_path: str) -> Path | None: + candidates: list[Path] = [] + if module_path.startswith("@app/"): + rel = module_path[len("@app/") :] + candidates.extend( + [ + repo_root / "frontend/src/core" / f"{rel}.ts", + repo_root / "frontend/src/core" / f"{rel}.tsx", + repo_root / "frontend/src/saas" / f"{rel}.ts", + repo_root / "frontend/src/saas" / f"{rel}.tsx", + repo_root / "frontend/src" / f"{rel}.ts", + repo_root / "frontend/src" / f"{rel}.tsx", + ] + ) + elif module_path.startswith("."): + base = (current_file.parent / module_path).resolve() + candidates.extend([Path(f"{base}.ts"), Path(f"{base}.tsx")]) + for candidate in candidates: + if candidate.exists(): + return candidate + return None + + +def _parse_literal_value(value: str, resolver: Callable[[str], dict[str, Any] | None]) -> Any: + value = value.strip() + if not value: + return None + if value.startswith("{") and value.endswith("}"): + return _parse_object_literal(value, resolver) + if value.startswith("[") and value.endswith("]"): + inner = value[1:-1].strip() + if not inner: + return [] + return [_parse_literal_value(item, resolver) for item in _split_top_level_items(inner)] + if value.startswith(("'", '"')) and value.endswith(("'", '"')): + return value[1:-1] + if value in {"true", "false"}: + return value == "true" + if value == "null": + return None + if re.fullmatch(r"-?\d+", value): + return int(value) + if re.fullmatch(r"-?\d+\.\d+", value): + return float(value) + resolved = resolver(value) + if resolved is not None: + return resolved + return None + + +def _parse_object_literal(obj_text: str, resolver: Callable[[str], dict[str, Any] | None]) -> dict[str, Any]: + body = obj_text.strip()[1:-1] + result: dict[str, Any] = {} + for item in _split_top_level_items(body): + if item.startswith("..."): + spread_name = item[3:].strip() + spread = resolver(spread_name) + if isinstance(spread, dict): + result.update(spread) + continue + if ":" not in item: + continue + key, raw_value = item.split(":", 1) + key = key.strip().strip("'\"") + result[key] = _parse_literal_value(raw_value.strip(), resolver) + return result + + +def _extract_imports(source: str) -> dict[str, str]: + imports: dict[str, str] = {} + for names, module_path in IMPORT_RE.findall(source): + for part in names.split(","): + segment = part.strip() + if not segment: + continue + if " as " in segment: + original, alias = [x.strip() for x in segment.split(" as ", 1)] + imports[alias] = module_path + imports[original] = module_path + else: + imports[segment] = module_path + return imports + + +def _resolve_object_identifier(repo_root: Path, file_path: Path, source: str, identifier: str) -> dict[str, Any] | None: + var_pattern = VAR_OBJ_RE_TEMPLATE.format(name=re.escape(identifier)) + block = _extract_block(source, var_pattern) + imports = _extract_imports(source) + + def resolver(name: str) -> dict[str, Any] | None: + local_block = _extract_block(source, VAR_OBJ_RE_TEMPLATE.format(name=re.escape(name))) + if local_block: + return _parse_object_literal(local_block, resolver) + import_path = imports.get(name) + if not import_path: + return None + resolved_file = _resolve_import_path(repo_root, file_path, import_path) + if not resolved_file: + return None + imported_source = resolved_file.read_text(encoding="utf-8") + return _resolve_object_identifier(repo_root, resolved_file, imported_source, name) + + if block: + return _parse_object_literal(block, resolver) + import_path = imports.get(identifier) + if not import_path: + return None + resolved_file = _resolve_import_path(repo_root, file_path, import_path) + if not resolved_file: + return None + imported_source = resolved_file.read_text(encoding="utf-8") + return _resolve_object_identifier(repo_root, resolved_file, imported_source, identifier) + + +def _infer_py_type(value: Any) -> str: + if isinstance(value, bool): + return "bool" + if isinstance(value, int): + return "int" + if isinstance(value, float): + return "float" + if isinstance(value, str): + return "str" + if isinstance(value, list): + return "list[Any]" + if isinstance(value, dict): + return "dict[str, Any]" + return "Any" + + +def _spec_is_none(spec: dict[str, Any]) -> bool: + return spec.get("kind") == "null" + + +def _py_type_from_spec(spec: dict[str, Any]) -> str: + kind = spec.get("kind") + if kind == "string": + return "str" + if kind == "number": + return "float" + if kind == "boolean": + return "bool" + if kind == "date": + return "str" + if kind == "enum": + values = spec.get("values") + if isinstance(values, list) and values: + literal_values = ", ".join(_py_repr(v) for v in values) + return f"Literal[{literal_values}]" + if kind == "ref": + ref_name = spec.get("name") + if isinstance(ref_name, str) and ref_name.endswith("Parameters"): + return f"{ref_name[:-10]}Params" + if kind == "array": + element = spec.get("element") + inner = _py_type_from_spec(element) if isinstance(element, dict) else "Any" + return f"list[{inner}]" + if kind == "object": + dict_value = spec.get("dictValue") + if isinstance(dict_value, dict): + inner = _py_type_from_spec(dict_value) + return f"dict[str, {inner}]" + properties = spec.get("properties") + if isinstance(properties, dict) and properties: + property_types = {_py_type_from_spec(p) for p in properties.values() if isinstance(p, dict)} + if len(property_types) == 1: + inner = next(iter(property_types)) + return f"dict[str, {inner}]" + return "dict[str, Any]" + if kind in {"null"}: + return "Any" + return "Any" + + +def _to_class_name(tool_id: str) -> str: + cleaned = re.sub(r"([a-z0-9])([A-Z])", r"\1 \2", tool_id) + cleaned = re.sub(r"[^A-Za-z0-9]+", " ", cleaned) + parts = [part.capitalize() for part in cleaned.split() if part] + return "".join(parts) + "Params" + + +def _to_snake_case(name: str) -> str: + snake = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name) + snake = re.sub(r"[^A-Za-z0-9]+", "_", snake).strip("_").lower() + if not snake: + snake = "param" + if snake[0].isdigit(): + snake = f"param_{snake}" + if keyword.iskeyword(snake): + snake = f"{snake}_" + return snake + + +def _build_field_name_map(params: dict[str, Any]) -> dict[str, str]: + field_map: dict[str, str] = {} + used: set[str] = set() + for original_key in sorted(params): + base_name = _to_snake_case(original_key) + candidate = base_name + suffix = 2 + while candidate in used: + candidate = f"{base_name}_{suffix}" + suffix += 1 + used.add(candidate) + field_map[original_key] = candidate + return field_map + + +def _to_enum_member_name(tool_id: str) -> str: + return _to_snake_case(tool_id).upper() + + +def _build_enum_member_map(specs: list[ToolModelSpec]) -> dict[str, str]: + member_map: dict[str, str] = {} + used: set[str] = set() + for spec in specs: + base_name = _to_enum_member_name(spec.tool_id) + candidate = base_name + suffix = 2 + while candidate in used: + candidate = f"{base_name}_{suffix}" + suffix += 1 + used.add(candidate) + member_map[spec.tool_id] = candidate + return member_map + + +def _py_repr(value: Any) -> str: + return ( + json.dumps(value, ensure_ascii=True).replace("true", "True").replace("false", "False").replace("null", "None") + ) + + +def discover_tool_specs(repo_root: Path) -> list[ToolModelSpec]: + frontend_dir = repo_root / "frontend" + extractor = frontend_dir / "scripts/export-tool-specs.ts" + command = ["node", "--import", "tsx", str(extractor)] + result = subprocess.run( + command, + check=True, + capture_output=True, + text=True, + cwd=str(frontend_dir), + ) + raw = json.loads(result.stdout) + specs: list[ToolModelSpec] = [] + for item in raw: + tool_id = item.get("tool_id") + if not isinstance(tool_id, str) or not tool_id: + continue + params = item.get("params") + param_types = item.get("param_types") + specs.append( + ToolModelSpec( + tool_id=tool_id, + params=params if isinstance(params, dict) else {}, + param_types=param_types if isinstance(param_types, dict) else {}, + ) + ) + return sorted(specs, key=lambda spec: spec.tool_id) + + +def write_models_module(out_path: Path, specs: list[ToolModelSpec]) -> None: + lines: list[str] = [ + TOOL_MODELS_HEADER, + "from __future__ import annotations\n\n", + "from enum import StrEnum\n", + "from typing import Any, Literal\n\n", + "from models.base import ApiModel\n", + ] + + class_names: dict[str, str] = {spec.tool_id: _to_class_name(spec.tool_id) for spec in specs} + class_name_to_tool_id = {name: tool_id for tool_id, name in class_names.items()} + + def extract_class_dependencies(spec: ToolModelSpec) -> set[str]: + deps: set[str] = set() + if not isinstance(spec.param_types, dict): + return deps + for entry in spec.param_types.values(): + if not isinstance(entry, dict): + continue + type_spec = entry + if "type" in entry and isinstance(entry.get("type"), dict): + type_spec = entry["type"] + if not isinstance(type_spec, dict): + continue + if type_spec.get("kind") != "ref": + continue + ref_name = type_spec.get("name") + if isinstance(ref_name, str) and ref_name.endswith("Parameters"): + ref_class = f"{ref_name[:-10]}Params" + if ref_class in class_name_to_tool_id: + deps.add(ref_class) + return deps + + dependencies_by_class: dict[str, set[str]] = {} + for spec in specs: + class_name = class_names[spec.tool_id] + dependencies_by_class[class_name] = extract_class_dependencies(spec) + + remaining = set(class_names.values()) + ordered_class_names: list[str] = [] + while remaining: + progress = False + for class_name in sorted(remaining): + deps = dependencies_by_class.get(class_name, set()) + if deps.issubset(set(ordered_class_names)): + ordered_class_names.append(class_name) + remaining.remove(class_name) + progress = True + break + if not progress: + ordered_class_names.extend(sorted(remaining)) + break + + ordered_specs = [next(spec for spec in specs if class_names[spec.tool_id] == name) for name in ordered_class_names] + + for spec in ordered_specs: + class_name = class_names[spec.tool_id] + lines.append(f"class {class_name}(ApiModel):\n") + all_param_keys = set(spec.params) + if isinstance(spec.param_types, dict): + all_param_keys.update(spec.param_types.keys()) + + if not all_param_keys: + lines.append(" pass\n\n\n") + continue + + field_name_map = _build_field_name_map({key: True for key in all_param_keys}) + for key in sorted(all_param_keys): + field_name = field_name_map[key] + value = spec.params.get(key) + type_spec = spec.param_types.get(key) if isinstance(spec.param_types, dict) else None + if isinstance(type_spec, dict): + py_type = _py_type_from_spec(type_spec) + else: + py_type = _infer_py_type(value) + + if value is None and (isinstance(type_spec, dict) and _spec_is_none(type_spec)): + if py_type != "Any" and "| None" not in py_type: + py_type = f"{py_type} | None" + lines.append(f" {field_name}: {py_type} = None\n") + elif value is None: + lines.append(f" {field_name}: {py_type} | None = None\n") + else: + if isinstance(type_spec, dict) and type_spec.get("kind") == "ref" and isinstance(value, dict): + lines.append(f" {field_name}: {py_type} = {py_type}.model_validate({_py_repr(value)})\n") + continue + lines.append(f" {field_name}: {py_type} = {_py_repr(value)}\n") + lines.append("\n\n") + + if class_names: + union_members = " | ".join(class_names[tool_id] for tool_id in sorted(class_names)) + lines.append(f"type ParamToolModel = {union_members}\n") + lines.append("type ParamToolModelType = type[ParamToolModel]\n\n") + else: + lines.append("type ParamToolModel = ApiModel\n") + lines.append("type ParamToolModelType = type[ParamToolModel]\n\n") + + enum_member_map = _build_enum_member_map(specs) + + lines.append("class OperationId(StrEnum):\n") + + for spec in specs: + lines.append(f" {enum_member_map[spec.tool_id]} = {spec.tool_id!r}\n") + + lines.extend( + [ + "\n\n", + "OPERATIONS: dict[OperationId, ParamToolModelType] = {\n", + ] + ) + + for spec in specs: + model_name = _to_class_name(spec.tool_id) + lines.append(f" OperationId.{enum_member_map[spec.tool_id]}: {model_name},\n") + lines.append("}\n") + out_path.write_text("".join(lines), encoding="utf-8") + + +def main() -> None: + parser = argparse.ArgumentParser(description="Generate tool models from frontend TypeScript tool definitions") + parser.add_argument("--spec", help="Deprecated (ignored)", default="") + parser.add_argument("--output", default="", help="Path to tool_models.py") + parser.add_argument("--ai-output", default="", help="Deprecated (ignored)") + args = parser.parse_args() + + repo_root = Path(__file__).resolve().parents[3] + specs = discover_tool_specs(repo_root) + + output_path = Path(args.output) if args.output else (repo_root / "docgen/backend/models/tool_models.py") + + write_models_module(output_path, specs) + print(f"Wrote {len(specs)} tool model specs") + + +if __name__ == "__main__": + main() diff --git a/engine/scripts/setup_env.py b/engine/scripts/setup_env.py new file mode 100644 index 0000000000..9626e2c8cf --- /dev/null +++ b/engine/scripts/setup_env.py @@ -0,0 +1,48 @@ +""" +Copies .env from .env.example if missing, and errors if any keys from the example +are absent from the actual .env file. + +Usage: + uv run scripts/setup_env.py +""" + +import os +import shutil +import sys +from pathlib import Path + +from dotenv import dotenv_values + +ROOT = Path(__file__).parent.parent +EXAMPLE_FILE = ROOT / "config" / ".env.example" +ENV_FILE = ROOT / ".env" + +print("setup-env: see engine/config/.env.example for documentation") + +if not EXAMPLE_FILE.exists(): + print(f"setup-env: {EXAMPLE_FILE.name} not found, skipping", file=sys.stderr) + sys.exit(0) + +if not ENV_FILE.exists(): + shutil.copy(EXAMPLE_FILE, ENV_FILE) + print("setup-env: created .env from .env.example") + +env_keys = set(dotenv_values(ENV_FILE).keys()) | set(os.environ.keys()) +example_keys = set(dotenv_values(EXAMPLE_FILE).keys()) +missing = sorted(example_keys - env_keys) + +if missing: + sys.exit( + "setup-env: .env is missing keys from .env.example:\n" + + "\n".join(f" {k}" for k in missing) + + "\n Add them manually or delete your local .env to re-copy from config/.env.example." + ) + +extra = sorted(k for k in dotenv_values(ENV_FILE) if k.startswith("STIRLING_") and k not in example_keys) +if extra: + print( + "setup-env: .env contains STIRLING_ keys not in config/.env.example:\n" + + "\n".join(f" {k}" for k in extra) + + "\n Add them to config/.env.example if they are intentional.", + file=sys.stderr, + ) diff --git a/engine/src/ai_generation.py b/engine/src/ai_generation.py new file mode 100644 index 0000000000..7a59234049 --- /dev/null +++ b/engine/src/ai_generation.py @@ -0,0 +1,363 @@ +from __future__ import annotations + +import json +import logging +from typing import Any + +import models +from config import SMART_MODEL, STREAMING_ENABLED +from format_prompts import get_format_prompt +from html_utils import inject_theme +from llm_utils import run_ai, stream_ai +from prompts import ( + field_values_system_prompt, + html_context_messages, + html_edit_system_prompt, + html_system_prompt, + outline_generator_system_prompt, + section_draft_system_prompt, + template_fill_html_system_prompt, +) + +logger = logging.getLogger(__name__) + + +def generate_outline_with_llm( + prompt: str, + document_type: str, + constraints: models.Constraint | None = None, +) -> models.OutlineResponse: + """ + Generate outline for a specific document type. + + document_type should already be detected - this function only generates the outline. + """ + constraint_text = "" + if constraints: + tone = constraints.tone + audience = constraints.audience + pages = constraints.page_count + constraint_text = f"Tone: {tone}. Audience: {audience}. Target pages: {pages}." + + # Try to get a format-specific extraction prompt + format_prompt, default_sections = get_format_prompt(document_type) + system_prompt = outline_generator_system_prompt(document_type, constraint_text, format_prompt, default_sections) + + messages: list[models.ChatMessage] = [ + models.ChatMessage(role="system", content=system_prompt), + models.ChatMessage(role="user", content=prompt), + ] + + parsed = run_ai( + SMART_MODEL, + messages, + models.OutlineResponse, + tag="outline", + ) + + if parsed: + logger.info( + "[OUTLINE] Generated outline doc_type=%s sections=%d filename=%s", + parsed.doc_type, + len(parsed.sections), + parsed.outline_filename, + ) + return parsed + + raise RuntimeError("AI outline generation failed.") + + +def generate_field_values( + prompt: str, + document_type: str, + fields: list[dict[str, Any]], + constraints: models.Constraint | None = None, +) -> list[dict[str, str]]: + constraint_text = "" + if constraints: + tone = constraints.tone + audience = constraints.audience + pages = constraints.page_count + constraint_text = f"Tone: {tone}. Audience: {audience}. Target pages: {pages}." + system_prompt = field_values_system_prompt(constraint_text) + messages: list[models.ChatMessage] = [ + models.ChatMessage(role="system", content=system_prompt), + models.ChatMessage(role="user", content=f"Document type: {document_type}"), + models.ChatMessage(role="user", content=f"Prompt:\n{prompt}"), + models.ChatMessage(role="user", content=f"Fields:\n{json.dumps(fields, ensure_ascii=True)}"), + ] + parsed = run_ai( + SMART_MODEL, + messages, + models.LLMFieldValuesResponse, + tag="field_values", + ) + if parsed: + return [{"label": item.label, "value": item.value} for item in parsed.fields] + + raise RuntimeError("AI field extraction failed.") + + +def generate_section_draft( + prompt: str, + document_type: str, + outline_text: str, + constraints: models.Constraint | None = None, +) -> list[models.DraftSection]: + constraint_text = "" + if constraints: + tone = constraints.tone + audience = constraints.audience + pages = constraints.page_count + constraint_text = f"Tone: {tone}. Audience: {audience}. Target pages: {pages}." + system_prompt = section_draft_system_prompt(constraint_text) + messages: list[models.ChatMessage] = [ + models.ChatMessage(role="system", content=system_prompt), + models.ChatMessage(role="user", content=f"Document type: {document_type}"), + models.ChatMessage(role="user", content=f"Outline:\n{outline_text}"), + models.ChatMessage(role="user", content=f"Prompt:\n{prompt}"), + ] + parsed = run_ai( + SMART_MODEL, + messages, + models.LLMDraftSectionsResponse, + tag="section_draft", + ) + return parsed.sections + + +# ── HTML generation ─────────────────────────────────────────────────────────── + + +def generate_template_fill_html_stream( + template_html: str, + document_type: str, + outline_text: str, + draft_sections: list[models.DraftSection] | None = None, + constraints: models.Constraint | None = None, + theme: dict[str, str] | None = None, + additional_instructions: str | None = None, + has_logo: bool = False, +): + """Fill an HTML template by replacing {{PLACEHOLDER}} tokens, streaming chunks.""" + constraints_text = "" + if constraints: + tone = constraints.tone + audience = constraints.audience + pages = constraints.page_count + constraints_text = f"Tone: {tone}. Audience: {audience}. Target pages: {pages}." + if draft_sections: + constraints_text += "\nUse the section content to inform placeholder values." + + # Inject theme before sending to AI if overrides are provided + if theme: + template_html = inject_theme(template_html, theme) + + system_prompt = template_fill_html_system_prompt(constraints_text) + messages: list[models.ChatMessage] = [ + models.ChatMessage(role="system", content=system_prompt), + models.ChatMessage(role="user", content=f"Document type: {document_type}"), + models.ChatMessage( + role="user", + content=( + "CRITICAL EXAMPLE — What to preserve vs. what to change:\n" + "If template has:\n" + "