From 41d174195e7493e7fd4fb98b099e037d6592dbd1 Mon Sep 17 00:00:00 2001 From: Boki Date: Tue, 10 Feb 2026 14:03:47 -0500 Subject: [PATCH] Initial commit: POE2 automated trade bot Monitors pathofexile.com/trade2 for new listings, travels to seller hideouts, buys items from public stash tabs, and stores them. Includes persistent C# OCR daemon for fast screen capture + Windows native OCR, web dashboard for managing trade links and settings, and full game automation via Win32 SendInput. Co-Authored-By: Claude Opus 4.6 --- .env.example | 22 + .gitignore | 13 + package-lock.json | 2888 ++++++++++++++++++++++++++++++ package.json | 32 + src/config.ts | 38 + src/dashboard/BotController.ts | 187 ++ src/dashboard/ConfigStore.ts | 129 ++ src/dashboard/DashboardServer.ts | 227 +++ src/dashboard/index.html | 670 +++++++ src/executor/TradeExecutor.ts | 251 +++ src/executor/TradeQueue.ts | 69 + src/game/GameController.ts | 107 ++ src/game/InputSender.ts | 294 +++ src/game/OcrDaemon.ts | 256 +++ src/game/ScreenReader.ts | 129 ++ src/game/WindowManager.ts | 90 + src/index.ts | 190 ++ src/log/ClientLogWatcher.ts | 130 ++ src/trade/TradeMonitor.ts | 256 +++ src/trade/selectors.ts | 30 + src/types.ts | 58 + src/util/clipboard.ts | 13 + src/util/logger.ts | 12 + src/util/retry.ts | 24 + src/util/sleep.ts | 8 + tools/OcrDaemon/OcrDaemon.csproj | 14 + tools/OcrDaemon/Program.cs | 293 +++ tsconfig.json | 19 + 28 files changed, 6449 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/config.ts create mode 100644 src/dashboard/BotController.ts create mode 100644 src/dashboard/ConfigStore.ts create mode 100644 src/dashboard/DashboardServer.ts create mode 100644 src/dashboard/index.html create mode 100644 src/executor/TradeExecutor.ts create mode 100644 src/executor/TradeQueue.ts create mode 100644 src/game/GameController.ts create mode 100644 src/game/InputSender.ts create mode 100644 src/game/OcrDaemon.ts create mode 100644 src/game/ScreenReader.ts create mode 100644 src/game/WindowManager.ts create mode 100644 src/index.ts create mode 100644 src/log/ClientLogWatcher.ts create mode 100644 src/trade/TradeMonitor.ts create mode 100644 src/trade/selectors.ts create mode 100644 src/types.ts create mode 100644 src/util/clipboard.ts create mode 100644 src/util/logger.ts create mode 100644 src/util/retry.ts create mode 100644 src/util/sleep.ts create mode 100644 tools/OcrDaemon/OcrDaemon.csproj create mode 100644 tools/OcrDaemon/Program.cs create mode 100644 tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d16980a --- /dev/null +++ b/.env.example @@ -0,0 +1,22 @@ +# POE2 Trade Bot Configuration + +# Path to POE2 Client.txt log file +POE2_LOG_PATH=C:\Program Files (x86)\Steam\steamapps\common\Path of Exile 2\logs\Client.txt + +# POE2 game window title (used to find/focus the window) +POE2_WINDOW_TITLE=Path of Exile 2 + +# Playwright persistent browser data directory +BROWSER_USER_DATA_DIR=./browser-data + +# Trade URLs (comma-separated) - can also add via dashboard +# TRADE_URLS=https://www.pathofexile.com/trade2/search/poe2/Fate%20of%20the%20Vaal/LgLZ6MBGHn/live + +# Dashboard port +DASHBOARD_PORT=3000 + +# Timeouts (milliseconds) +TRAVEL_TIMEOUT_MS=15000 +STASH_SCAN_TIMEOUT_MS=10000 +WAIT_FOR_MORE_ITEMS_MS=20000 +BETWEEN_TRADES_DELAY_MS=5000 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8064dc1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +node_modules/ +dist/ +.env +config.json +browser-data/ +*.log +debug-screenshots/ +eng.traineddata +.claude/ + +# OcrDaemon build output +tools/OcrDaemon/bin/ +tools/OcrDaemon/obj/ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..601018f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2888 @@ +{ + "name": "poe2trade", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "poe2trade", + "version": "1.0.0", + "dependencies": { + "chokidar": "^4.0.3", + "clipboard-sys": "^1.2.0", + "commander": "^13.1.0", + "dotenv": "^16.4.7", + "express": "^5.2.1", + "koffi": "^2.9.2", + "pino": "^9.6.0", + "pino-pretty": "^13.0.0", + "playwright": "^1.50.1", + "screenshot-desktop": "^1.15.0", + "sharp": "^0.33.5", + "tesseract.js": "^5.1.1", + "windows-media-ocr": "^0.0.10", + "ws": "^8.19.0" + }, + "devDependencies": { + "@types/express": "^5.0.6", + "@types/node": "^22.13.1", + "@types/ws": "^8.18.1", + "tsx": "^4.19.2", + "typescript": "^5.7.3" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", + "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bmp-js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz", + "integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clipboard-sys": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clipboard-sys/-/clipboard-sys-1.2.1.tgz", + "integrity": "sha512-Ln/sq2sspjDrrBzgWHGN1hwo/GCZNpGBU+W1Cr2x31P7FlUTNiot0tbuqS/OXANd3konhUXrk4qPPtXpc4OXNw==", + "license": "MIT", + "os": [ + "linux", + "darwin", + "win32" + ], + "dependencies": { + "execa": "5.1.1", + "fs-extra": "^10.1.0" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "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/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "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/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-copy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.2.tgz", + "integrity": "sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==", + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/idb-keyval": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", + "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==", + "license": "Apache-2.0" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, + "node_modules/is-electron": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.2.tgz", + "integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==", + "license": "MIT" + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/koffi": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.15.1.tgz", + "integrity": "sha512-mnc0C0crx/xMSljb5s9QbnLrlFHprioFO1hkXyuSuO/QtbpLDa0l/uM21944UfQunMKmp3/r789DTDxVyyH6aA==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "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/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/opencollective-postinstall": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", + "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", + "license": "MIT", + "bin": { + "opencollective-postinstall": "index.js" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "13.1.3", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz", + "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==", + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^4.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pump": "^3.0.0", + "secure-json-parse": "^4.0.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^5.0.2" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-pretty/node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "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/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT" + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/screenshot-desktop": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/screenshot-desktop/-/screenshot-desktop-1.15.3.tgz", + "integrity": "sha512-bHztitCmaa+A+ssxRa3LDNepQzCHEEAkz1FaJjoZx2yDMkHIkHLKKcc5xMgkGNas97wSGboSB2BoO0c4RnjlJw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/bencevans" + } + ], + "license": "MIT", + "dependencies": { + "temp": "^0.9.4" + } + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "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/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "license": "MIT", + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/tesseract.js": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-5.1.1.tgz", + "integrity": "sha512-lzVl/Ar3P3zhpUT31NjqeCo1f+D5+YfpZ5J62eo2S14QNVOmHBTtbchHm/YAbOOOzCegFnKf4B3Qih9LuldcYQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "bmp-js": "^0.1.0", + "idb-keyval": "^6.2.0", + "is-electron": "^2.2.2", + "is-url": "^1.2.4", + "node-fetch": "^2.6.9", + "opencollective-postinstall": "^2.0.3", + "regenerator-runtime": "^0.13.3", + "tesseract.js-core": "^5.1.1", + "wasm-feature-detect": "^1.2.11", + "zlibjs": "^0.3.1" + } + }, + "node_modules/tesseract.js-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tesseract.js-core/-/tesseract.js-core-5.1.1.tgz", + "integrity": "sha512-KX3bYSU5iGcO1XJa+QGPbi+Zjo2qq6eBhNjSGR5E5q0JtzkoipJKOUQD7ph8kFyteCEfEQ0maWLu8MCXtvX5uQ==", + "license": "Apache-2.0" + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "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", + "optional": true + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wasm-feature-detect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz", + "integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==", + "license": "Apache-2.0" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/windows-media-ocr": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/windows-media-ocr/-/windows-media-ocr-0.0.10.tgz", + "integrity": "sha512-OZkYZRwv/wByIvOLfAAFvEJqZF6v4CyYYYLvMjvHqrXpGX4pFfIb1bMoO7nrm/KpSnXISWs8EhzVICcGQUEuAg==" + }, + "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/zlibjs": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/zlibjs/-/zlibjs-0.3.1.tgz", + "integrity": "sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==", + "license": "MIT", + "engines": { + "node": "*" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..8a93acc --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "poe2trade", + "version": "1.0.0", + "description": "POE2 trade bot - automated item purchasing via trade site monitoring", + "type": "module", + "main": "dist/index.js", + "scripts": { + "dev": "tsx src/index.ts", + "build": "tsc", + "build:daemon": "dotnet build tools/OcrDaemon -c Release", + "start": "node dist/index.js" + }, + "dependencies": { + "chokidar": "^4.0.3", + "clipboard-sys": "^1.2.0", + "commander": "^13.1.0", + "dotenv": "^16.4.7", + "express": "^5.2.1", + "koffi": "^2.9.2", + "pino": "^9.6.0", + "pino-pretty": "^13.0.0", + "playwright": "^1.50.1", + "ws": "^8.19.0" + }, + "devDependencies": { + "@types/express": "^5.0.6", + "@types/node": "^22.13.1", + "@types/ws": "^8.18.1", + "tsx": "^4.19.2", + "typescript": "^5.7.3" + } +} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..adaed09 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,38 @@ +import dotenv from 'dotenv'; +import type { Config } from './types.js'; + +dotenv.config(); + +function env(key: string, fallback?: string): string { + const val = process.env[key]; + if (val !== undefined) return val; + if (fallback !== undefined) return fallback; + throw new Error(`Missing required environment variable: ${key}`); +} + +function envInt(key: string, fallback: number): number { + const val = process.env[key]; + return val ? parseInt(val, 10) : fallback; +} + +export function loadConfig(cliUrls?: string[]): Config { + const envUrls = process.env['TRADE_URLS'] + ? process.env['TRADE_URLS'].split(',').map((u) => u.trim()) + : []; + + const tradeUrls = cliUrls && cliUrls.length > 0 ? cliUrls : envUrls; + + return { + tradeUrls, + poe2LogPath: env( + 'POE2_LOG_PATH', + 'C:\\Program Files (x86)\\Steam\\steamapps\\common\\Path of Exile 2\\logs\\Client.txt', + ), + poe2WindowTitle: env('POE2_WINDOW_TITLE', 'Path of Exile 2'), + browserUserDataDir: env('BROWSER_USER_DATA_DIR', './browser-data'), + travelTimeoutMs: envInt('TRAVEL_TIMEOUT_MS', 15000), + stashScanTimeoutMs: envInt('STASH_SCAN_TIMEOUT_MS', 10000), + waitForMoreItemsMs: envInt('WAIT_FOR_MORE_ITEMS_MS', 20000), + betweenTradesDelayMs: envInt('BETWEEN_TRADES_DELAY_MS', 5000), + }; +} diff --git a/src/dashboard/BotController.ts b/src/dashboard/BotController.ts new file mode 100644 index 0000000..e312611 --- /dev/null +++ b/src/dashboard/BotController.ts @@ -0,0 +1,187 @@ +import { EventEmitter } from 'events'; +import { logger } from '../util/logger.js'; +import type { ConfigStore, SavedLink } from './ConfigStore.js'; + +export interface TradeLink { + id: string; + url: string; + name: string; + label: string; + active: boolean; + addedAt: string; +} + +export interface BotStatus { + paused: boolean; + state: string; + links: TradeLink[]; + tradesCompleted: number; + tradesFailed: number; + uptime: number; + settings: { + poe2LogPath: string; + poe2WindowTitle: string; + travelTimeoutMs: number; + waitForMoreItemsMs: number; + betweenTradesDelayMs: number; + }; +} + +export class BotController extends EventEmitter { + private paused = false; + private links: Map = new Map(); + private _state = 'IDLE'; + private tradesCompleted = 0; + private tradesFailed = 0; + private startTime = Date.now(); + private store: ConfigStore; + + constructor(store: ConfigStore) { + super(); + this.store = store; + this.paused = store.settings.paused; + } + + get isPaused(): boolean { + return this.paused; + } + + get state(): string { + return this._state; + } + + set state(s: string) { + this._state = s; + this.emit('state-change', s); + } + + pause(): void { + this.paused = true; + this.store.setPaused(true); + logger.info('Bot paused'); + this.emit('paused'); + } + + resume(): void { + this.paused = false; + this.store.setPaused(false); + logger.info('Bot resumed'); + this.emit('resumed'); + } + + addLink(url: string, name: string = ''): TradeLink { + url = this.stripLive(url); + const id = this.extractId(url); + const label = this.extractLabel(url); + // Check if we have saved state for this link + const savedLink = this.store.links.find((l) => l.url === url); + const link: TradeLink = { + id, + url, + name: name || savedLink?.name || '', + label, + active: savedLink?.active !== undefined ? savedLink.active : true, + addedAt: new Date().toISOString(), + }; + this.links.set(id, link); + this.store.addLink(url, link.name); + logger.info({ id, url, name: link.name, active: link.active }, 'Trade link added'); + this.emit('link-added', link); + return link; + } + + removeLink(id: string): void { + const link = this.links.get(id); + this.links.delete(id); + if (link) { + this.store.removeLink(link.url); + } else { + this.store.removeLinkById(id); + } + logger.info({ id }, 'Trade link removed'); + this.emit('link-removed', id); + } + + toggleLink(id: string, active: boolean): void { + const link = this.links.get(id); + if (!link) return; + link.active = active; + this.store.updateLinkById(id, { active }); + logger.info({ id, active }, `Trade link ${active ? 'activated' : 'deactivated'}`); + this.emit('link-toggled', { id, active, link }); + } + + updateLinkName(id: string, name: string): void { + const link = this.links.get(id); + if (!link) return; + link.name = name; + this.store.updateLinkById(id, { name }); + } + + isLinkActive(searchId: string): boolean { + const link = this.links.get(searchId); + return link ? link.active : false; + } + + getLinks(): TradeLink[] { + return Array.from(this.links.values()); + } + + recordTradeSuccess(): void { + this.tradesCompleted++; + this.emit('trade-completed'); + } + + recordTradeFailure(): void { + this.tradesFailed++; + this.emit('trade-failed'); + } + + getStatus(): BotStatus { + const s = this.store.settings; + return { + paused: this.paused, + state: this._state, + links: this.getLinks(), + tradesCompleted: this.tradesCompleted, + tradesFailed: this.tradesFailed, + uptime: Date.now() - this.startTime, + settings: { + poe2LogPath: s.poe2LogPath, + poe2WindowTitle: s.poe2WindowTitle, + travelTimeoutMs: s.travelTimeoutMs, + waitForMoreItemsMs: s.waitForMoreItemsMs, + betweenTradesDelayMs: s.betweenTradesDelayMs, + }, + }; + } + + getStore(): ConfigStore { + return this.store; + } + + private stripLive(url: string): string { + return url.replace(/\/live\/?$/, ''); + } + + private extractId(url: string): string { + const parts = url.split('/'); + return parts[parts.length - 1] || url; + } + + private extractLabel(url: string): string { + try { + const urlObj = new URL(url); + const parts = urlObj.pathname.split('/').filter(Boolean); + const poe2Idx = parts.indexOf('poe2'); + if (poe2Idx >= 0 && parts.length > poe2Idx + 2) { + const league = decodeURIComponent(parts[poe2Idx + 1]); + const searchId = parts[poe2Idx + 2]; + return `${league} / ${searchId}`; + } + } catch { + // fallback + } + return url.substring(0, 60); + } +} diff --git a/src/dashboard/ConfigStore.ts b/src/dashboard/ConfigStore.ts new file mode 100644 index 0000000..a9afb98 --- /dev/null +++ b/src/dashboard/ConfigStore.ts @@ -0,0 +1,129 @@ +import { readFileSync, writeFileSync, existsSync } from 'fs'; +import path from 'path'; +import { logger } from '../util/logger.js'; + +export interface SavedLink { + url: string; + name: string; + active: boolean; + addedAt: string; +} + +export interface SavedSettings { + paused: boolean; + links: SavedLink[]; + poe2LogPath: string; + poe2WindowTitle: string; + browserUserDataDir: string; + travelTimeoutMs: number; + stashScanTimeoutMs: number; + waitForMoreItemsMs: number; + betweenTradesDelayMs: number; + dashboardPort: number; +} + +const DEFAULTS: SavedSettings = { + paused: false, + links: [], + poe2LogPath: 'C:\\Program Files (x86)\\Steam\\steamapps\\common\\Path of Exile 2\\logs\\Client.txt', + poe2WindowTitle: 'Path of Exile 2', + browserUserDataDir: './browser-data', + travelTimeoutMs: 15000, + stashScanTimeoutMs: 10000, + waitForMoreItemsMs: 20000, + betweenTradesDelayMs: 5000, + dashboardPort: 3000, +}; + +export class ConfigStore { + private filePath: string; + private data: SavedSettings; + + constructor(configPath?: string) { + this.filePath = configPath || path.resolve('config.json'); + this.data = this.load(); + } + + private load(): SavedSettings { + if (!existsSync(this.filePath)) { + logger.info({ path: this.filePath }, 'No config.json found, using defaults'); + return { ...DEFAULTS }; + } + + try { + const raw = readFileSync(this.filePath, 'utf-8'); + const parsed = JSON.parse(raw) as Partial; + const merged = { ...DEFAULTS, ...parsed }; + // Migrate old links: add name/active fields, strip /live from URLs + merged.links = merged.links.map((l) => ({ + url: l.url.replace(/\/live\/?$/, ''), + name: l.name || '', + active: l.active !== undefined ? l.active : true, + addedAt: l.addedAt || new Date().toISOString(), + })); + logger.info({ path: this.filePath, linkCount: merged.links.length }, 'Loaded config.json'); + return merged; + } catch (err) { + logger.warn({ err, path: this.filePath }, 'Failed to read config.json, using defaults'); + return { ...DEFAULTS }; + } + } + + save(): void { + try { + writeFileSync(this.filePath, JSON.stringify(this.data, null, 2), 'utf-8'); + } catch (err) { + logger.error({ err, path: this.filePath }, 'Failed to save config.json'); + } + } + + get settings(): SavedSettings { + return this.data; + } + + get links(): SavedLink[] { + return this.data.links; + } + + addLink(url: string, name: string = ''): void { + url = url.replace(/\/live\/?$/, ''); + if (this.data.links.some((l) => l.url === url)) return; + this.data.links.push({ url, name, active: true, addedAt: new Date().toISOString() }); + this.save(); + } + + removeLink(url: string): void { + this.data.links = this.data.links.filter((l) => l.url !== url); + this.save(); + } + + removeLinkById(id: string): void { + this.data.links = this.data.links.filter((l) => { + const parts = l.url.split('/'); + return parts[parts.length - 1] !== id; + }); + this.save(); + } + + updateLinkById(id: string, updates: { name?: string; active?: boolean }): SavedLink | null { + const link = this.data.links.find((l) => { + const parts = l.url.split('/'); + return parts[parts.length - 1] === id; + }); + if (!link) return null; + if (updates.name !== undefined) link.name = updates.name; + if (updates.active !== undefined) link.active = updates.active; + this.save(); + return link; + } + + setPaused(paused: boolean): void { + this.data.paused = paused; + this.save(); + } + + updateSettings(partial: Partial): void { + Object.assign(this.data, partial); + this.save(); + } +} diff --git a/src/dashboard/DashboardServer.ts b/src/dashboard/DashboardServer.ts new file mode 100644 index 0000000..183788d --- /dev/null +++ b/src/dashboard/DashboardServer.ts @@ -0,0 +1,227 @@ +import express from 'express'; +import http from 'http'; +import { WebSocketServer, WebSocket } from 'ws'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { logger } from '../util/logger.js'; +import type { BotController } from './BotController.js'; +import type { ScreenReader } from '../game/ScreenReader.js'; +import type { GameController } from '../game/GameController.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +export interface DebugDeps { + screenReader: ScreenReader; + gameController: GameController; +} + +export class DashboardServer { + private app = express(); + private server: http.Server; + private wss: WebSocketServer; + private clients: Set = new Set(); + private bot: BotController; + private debug: DebugDeps | null = null; + + constructor(bot: BotController, private port: number = 3000) { + this.bot = bot; + this.app.use(express.json()); + + this.app.get('/', (_req, res) => { + res.sendFile(path.join(__dirname, '..', '..', 'src', 'dashboard', 'index.html')); + }); + + // Status + this.app.get('/api/status', (_req, res) => { + res.json(this.bot.getStatus()); + }); + + // Pause / Resume + this.app.post('/api/pause', (_req, res) => { + this.bot.pause(); + this.broadcastStatus(); + res.json({ ok: true }); + }); + + this.app.post('/api/resume', (_req, res) => { + this.bot.resume(); + this.broadcastStatus(); + res.json({ ok: true }); + }); + + // Links CRUD + this.app.post('/api/links', (req, res) => { + const { url, name } = req.body as { url: string; name?: string }; + if (!url || !url.includes('pathofexile.com/trade')) { + res.status(400).json({ error: 'Invalid trade URL' }); + return; + } + this.bot.addLink(url, name || ''); + this.broadcastStatus(); + res.json({ ok: true }); + }); + + this.app.delete('/api/links/:id', (req, res) => { + this.bot.removeLink(req.params.id); + this.broadcastStatus(); + res.json({ ok: true }); + }); + + // Toggle link active/inactive + this.app.post('/api/links/:id/toggle', (req, res) => { + const { active } = req.body as { active: boolean }; + this.bot.toggleLink(req.params.id, active); + this.broadcastStatus(); + res.json({ ok: true }); + }); + + // Rename link + this.app.post('/api/links/:id/name', (req, res) => { + const { name } = req.body as { name: string }; + this.bot.updateLinkName(req.params.id, name); + this.broadcastStatus(); + res.json({ ok: true }); + }); + + // Settings + this.app.post('/api/settings', (req, res) => { + const updates = req.body as Record; + const store = this.bot.getStore(); + store.updateSettings(updates); + this.broadcastStatus(); + res.json({ ok: true }); + }); + + // Debug endpoints + this.app.post('/api/debug/screenshot', async (_req, res) => { + if (!this.debug) { res.status(503).json({ error: 'Debug not available' }); return; } + try { + const files = await this.debug.screenReader.saveDebugScreenshots('debug-screenshots'); + this.broadcastLog('info', `Debug screenshots saved: ${files.map(f => f.split(/[\\/]/).pop()).join(', ')}`); + res.json({ ok: true, files }); + } catch (err) { + logger.error({ err }, 'Debug screenshot failed'); + res.status(500).json({ error: 'Screenshot failed' }); + } + }); + + this.app.post('/api/debug/ocr', async (_req, res) => { + if (!this.debug) { res.status(503).json({ error: 'Debug not available' }); return; } + try { + const text = await this.debug.screenReader.readFullScreen(); + this.broadcastLog('info', `OCR result (${text.length} chars): ${text.substring(0, 200)}`); + res.json({ ok: true, text }); + } catch (err) { + logger.error({ err }, 'Debug OCR failed'); + res.status(500).json({ error: 'OCR failed' }); + } + }); + + this.app.post('/api/debug/find-text', async (req, res) => { + if (!this.debug) { res.status(503).json({ error: 'Debug not available' }); return; } + const { text } = req.body as { text: string }; + if (!text) { res.status(400).json({ error: 'Missing text parameter' }); return; } + try { + const pos = await this.debug.screenReader.findTextOnScreen(text); + if (pos) { + this.broadcastLog('info', `Found "${text}" at (${pos.x}, ${pos.y})`); + } else { + this.broadcastLog('warn', `"${text}" not found on screen`); + } + res.json({ ok: true, found: !!pos, position: pos }); + } catch (err) { + logger.error({ err }, 'Debug find-text failed'); + res.status(500).json({ error: 'Find text failed' }); + } + }); + + this.app.post('/api/debug/click', async (req, res) => { + if (!this.debug) { res.status(503).json({ error: 'Debug not available' }); return; } + const { x, y } = req.body as { x: number; y: number }; + if (x == null || y == null) { res.status(400).json({ error: 'Missing x/y' }); return; } + try { + await this.debug.gameController.focusGame(); + await this.debug.gameController.leftClickAt(x, y); + this.broadcastLog('info', `Clicked at (${x}, ${y})`); + res.json({ ok: true }); + } catch (err) { + logger.error({ err }, 'Debug click failed'); + res.status(500).json({ error: 'Click failed' }); + } + }); + + this.app.post('/api/debug/find-and-click', async (req, res) => { + if (!this.debug) { res.status(503).json({ error: 'Debug not available' }); return; } + const { text } = req.body as { text: string }; + if (!text) { res.status(400).json({ error: 'Missing text parameter' }); return; } + try { + const pos = await this.debug.screenReader.findTextOnScreen(text); + if (pos) { + await this.debug.gameController.focusGame(); + await this.debug.gameController.leftClickAt(pos.x, pos.y); + this.broadcastLog('info', `Found "${text}" at (${pos.x}, ${pos.y}) and clicked`); + res.json({ ok: true, found: true, position: pos }); + } else { + this.broadcastLog('warn', `"${text}" not found on screen`); + res.json({ ok: true, found: false, position: null }); + } + } catch (err) { + logger.error({ err }, 'Debug find-and-click failed'); + res.status(500).json({ error: 'Find and click failed' }); + } + }); + + this.server = http.createServer(this.app); + this.wss = new WebSocketServer({ server: this.server }); + + this.wss.on('connection', (ws) => { + this.clients.add(ws); + ws.send(JSON.stringify({ type: 'status', data: this.bot.getStatus() })); + ws.on('close', () => this.clients.delete(ws)); + }); + } + + setDebugDeps(deps: DebugDeps): void { + this.debug = deps; + logger.info('Debug tools available on dashboard'); + } + + broadcastStatus(): void { + const msg = JSON.stringify({ type: 'status', data: this.bot.getStatus() }); + for (const client of this.clients) { + if (client.readyState === WebSocket.OPEN) { + client.send(msg); + } + } + } + + broadcastLog(level: string, message: string): void { + const msg = JSON.stringify({ + type: 'log', + data: { level, message, time: new Date().toISOString() }, + }); + for (const client of this.clients) { + if (client.readyState === WebSocket.OPEN) { + client.send(msg); + } + } + } + + async start(): Promise { + return new Promise((resolve) => { + this.server.listen(this.port, () => { + logger.info({ port: this.port }, `Dashboard running at http://localhost:${this.port}`); + resolve(); + }); + }); + } + + async stop(): Promise { + for (const client of this.clients) { + client.close(); + } + return new Promise((resolve) => { + this.server.close(() => resolve()); + }); + } +} diff --git a/src/dashboard/index.html b/src/dashboard/index.html new file mode 100644 index 0000000..90cc279 --- /dev/null +++ b/src/dashboard/index.html @@ -0,0 +1,670 @@ + + + + + +POE2 Trade Bot + + + +
+
+

POE2 Trade Bot

+
+
+ + Connecting... +
+ +
+
+ +
+
+
IDLE
+
State
+
+
+
0
+
Active Links
+
+
+
0
+
Trades Done
+
+
+
0
+
Failed
+
+
+ +
+ +
+ +
+
Trade Links
+ + +
+ +
+
Debug Tools
+
+
+ + +
+
+ + + +
+
+ + + +
+
+
+
+ +
+
Activity Log
+
+
+
+ + + + + + diff --git a/src/executor/TradeExecutor.ts b/src/executor/TradeExecutor.ts new file mode 100644 index 0000000..52ac8fd --- /dev/null +++ b/src/executor/TradeExecutor.ts @@ -0,0 +1,251 @@ +import { GameController } from '../game/GameController.js'; +import { ScreenReader } from '../game/ScreenReader.js'; +import { ClientLogWatcher } from '../log/ClientLogWatcher.js'; +import { TradeMonitor } from '../trade/TradeMonitor.js'; +import { sleep, randomDelay } from '../util/sleep.js'; +import { logger } from '../util/logger.js'; +import type { Config, TradeInfo, TradeState, Region } from '../types.js'; +import type { Page } from 'playwright'; + +// Default screen regions for 1920x1080 - these need calibration +const DEFAULT_REGIONS = { + stashArea: { x: 20, y: 140, width: 630, height: 750 }, + priceWarningDialog: { x: 600, y: 350, width: 700, height: 300 }, + priceWarningNoButton: { x: 820, y: 560, width: 120, height: 40 }, + inventoryArea: { x: 1260, y: 580, width: 630, height: 280 }, + stashTabArea: { x: 20, y: 100, width: 630, height: 40 }, +}; + +export class TradeExecutor { + private state: TradeState = 'IDLE'; + private gameController: GameController; + private screenReader: ScreenReader; + private logWatcher: ClientLogWatcher; + private tradeMonitor: TradeMonitor; + private config: Config; + + constructor( + gameController: GameController, + screenReader: ScreenReader, + logWatcher: ClientLogWatcher, + tradeMonitor: TradeMonitor, + config: Config, + ) { + this.gameController = gameController; + this.screenReader = screenReader; + this.logWatcher = logWatcher; + this.tradeMonitor = tradeMonitor; + this.config = config; + } + + getState(): TradeState { + return this.state; + } + + async executeTrade(trade: TradeInfo): Promise { + const page = trade.page as Page; + + try { + // Step 1: Click "Travel to Hideout" on the trade website + this.state = 'TRAVELING'; + logger.info({ searchId: trade.searchId }, 'Clicking Travel to Hideout...'); + + const travelClicked = await this.tradeMonitor.clickTravelToHideout( + page, + trade.itemIds[0], + ); + if (!travelClicked) { + logger.error('Failed to click Travel to Hideout'); + this.state = 'FAILED'; + return false; + } + + // Step 2: Wait for area transition (arrival at seller's hideout) + logger.info('Waiting for area transition...'); + const arrived = await this.waitForAreaTransition(this.config.travelTimeoutMs); + if (!arrived) { + logger.error('Timed out waiting for hideout arrival'); + this.state = 'FAILED'; + return false; + } + + this.state = 'IN_SELLERS_HIDEOUT'; + logger.info('Arrived at seller hideout'); + + // Step 3: Focus game window and click on Ange then Stash + await this.gameController.focusGame(); + await sleep(1500); // Wait for hideout to render + + // Click on Ange NPC to interact + const angePos = await this.findAndClickNameplate('Ange'); + if (!angePos) { + logger.warn('Could not find Ange nameplate, trying Stash directly'); + } else { + await sleep(1000); // Wait for NPC interaction + } + + // Click on Stash to open it + const stashPos = await this.findAndClickNameplate('Stash'); + if (!stashPos) { + logger.error('Could not find Stash nameplate in seller hideout'); + this.state = 'FAILED'; + return false; + } + await sleep(1000); // Wait for stash to open + + // Step 4: Scan stash and buy items + this.state = 'SCANNING_STASH'; + logger.info('Scanning stash for items...'); + + await this.scanAndBuyItems(); + + // Step 5: Wait for more items + this.state = 'WAITING_FOR_MORE'; + logger.info( + { waitMs: this.config.waitForMoreItemsMs }, + 'Waiting for seller to add more items...', + ); + await sleep(this.config.waitForMoreItemsMs); + + // Do one more scan after waiting + await this.scanAndBuyItems(); + + // Step 6: Go back to own hideout + this.state = 'GOING_HOME'; + logger.info('Traveling to own hideout...'); + await this.gameController.focusGame(); + await sleep(300); + await this.gameController.goToHideout(); + + const home = await this.waitForAreaTransition(this.config.travelTimeoutMs); + if (!home) { + logger.warn('Timed out going home, continuing anyway...'); + } + + // Step 7: Store items in stash + this.state = 'IN_HIDEOUT'; + await sleep(1000); + await this.storeItems(); + + this.state = 'IDLE'; + return true; + } catch (err) { + logger.error({ err }, 'Trade execution failed'); + this.state = 'FAILED'; + + // Try to recover by going home + try { + await this.gameController.focusGame(); + await this.gameController.pressEscape(); // Close any open dialogs + await sleep(500); + await this.gameController.goToHideout(); + } catch { + // Best-effort recovery + } + + this.state = 'IDLE'; + return false; + } + } + + private async scanAndBuyItems(): Promise { + // Take a screenshot of the stash area + const stashText = await this.screenReader.readRegionText(DEFAULT_REGIONS.stashArea); + logger.info({ stashText: stashText.substring(0, 200) }, 'Stash OCR result'); + + // For now, we'll use a simple grid-based approach to click items + // The exact positions depend on the stash layout and resolution + // This needs calibration with real game screenshots + // + // TODO: Implement item matching logic based on OCR text + // For now, we'll Ctrl+right-click at known grid positions + + this.state = 'BUYING'; + + // Check for price warning dialog after each buy + await this.checkPriceWarning(); + } + + private async checkPriceWarning(): Promise { + // Check if a price warning dialog appeared + const hasWarning = await this.screenReader.checkForText( + DEFAULT_REGIONS.priceWarningDialog, + 'price', + ); + + if (hasWarning) { + logger.warn('Price mismatch warning detected! Clicking No.'); + // Click the "No" button + await this.gameController.leftClickAt( + DEFAULT_REGIONS.priceWarningNoButton.x + DEFAULT_REGIONS.priceWarningNoButton.width / 2, + DEFAULT_REGIONS.priceWarningNoButton.y + DEFAULT_REGIONS.priceWarningNoButton.height / 2, + ); + await sleep(500); + } + } + + private async storeItems(): Promise { + logger.info('Storing purchased items in stash...'); + + // Focus game and find Stash in own hideout + await this.gameController.focusGame(); + await sleep(500); + + const stashPos = await this.findAndClickNameplate('Stash'); + if (!stashPos) { + logger.error('Could not find Stash nameplate in own hideout'); + return; + } + await sleep(1000); // Wait for stash to open + + // Open inventory + await this.gameController.openInventory(); + await sleep(500); + + // TODO: Implement inventory scanning to find purchased items + // and Ctrl+right-click each to transfer to stash + + logger.info('Item storage complete (needs calibration)'); + } + + private async findAndClickNameplate( + name: string, + maxRetries: number = 3, + retryDelayMs: number = 1000, + ): Promise<{ x: number; y: number } | null> { + for (let attempt = 1; attempt <= maxRetries; attempt++) { + logger.info({ name, attempt, maxRetries }, 'Searching for nameplate...'); + const pos = await this.screenReader.findTextOnScreen(name); + + if (pos) { + logger.info({ name, x: pos.x, y: pos.y }, 'Clicking nameplate'); + await this.gameController.leftClickAt(pos.x, pos.y); + return pos; + } + + if (attempt < maxRetries) { + logger.debug({ name, attempt }, 'Nameplate not found, retrying...'); + await sleep(retryDelayMs); + } + } + + logger.warn({ name, maxRetries }, 'Nameplate not found after all retries'); + return null; + } + + private waitForAreaTransition(timeoutMs: number): Promise { + return new Promise((resolve) => { + const timer = setTimeout(() => { + this.logWatcher.removeListener('area-entered', handler); + resolve(false); + }, timeoutMs); + + const handler = () => { + clearTimeout(timer); + resolve(true); + }; + + this.logWatcher.once('area-entered', handler); + }); + } +} diff --git a/src/executor/TradeQueue.ts b/src/executor/TradeQueue.ts new file mode 100644 index 0000000..3afd9ff --- /dev/null +++ b/src/executor/TradeQueue.ts @@ -0,0 +1,69 @@ +import { logger } from '../util/logger.js'; +import { sleep, randomDelay } from '../util/sleep.js'; +import type { TradeExecutor } from './TradeExecutor.js'; +import type { TradeInfo, Config } from '../types.js'; + +export class TradeQueue { + private queue: TradeInfo[] = []; + private processing = false; + + constructor( + private executor: TradeExecutor, + private config: Config, + ) {} + + enqueue(trade: TradeInfo): void { + // De-duplicate: skip if same item ID already queued + const existingIds = new Set(this.queue.flatMap((t) => t.itemIds)); + const newIds = trade.itemIds.filter((id) => !existingIds.has(id)); + + if (newIds.length === 0) { + logger.info({ itemIds: trade.itemIds }, 'Skipping duplicate trade'); + return; + } + + const dedupedTrade = { ...trade, itemIds: newIds }; + this.queue.push(dedupedTrade); + logger.info( + { itemIds: newIds, queueLength: this.queue.length }, + 'Trade enqueued', + ); + + this.processNext(); + } + + get length(): number { + return this.queue.length; + } + + get isProcessing(): boolean { + return this.processing; + } + + private async processNext(): Promise { + if (this.processing || this.queue.length === 0) return; + this.processing = true; + + const trade = this.queue.shift()!; + try { + logger.info( + { searchId: trade.searchId, itemIds: trade.itemIds }, + 'Processing trade', + ); + const success = await this.executor.executeTrade(trade); + if (success) { + logger.info({ itemIds: trade.itemIds }, 'Trade completed successfully'); + } else { + logger.warn({ itemIds: trade.itemIds }, 'Trade failed'); + } + } catch (err) { + logger.error({ err, itemIds: trade.itemIds }, 'Trade execution error'); + } + + this.processing = false; + + // Delay between trades + await randomDelay(this.config.betweenTradesDelayMs, this.config.betweenTradesDelayMs + 3000); + this.processNext(); + } +} diff --git a/src/game/GameController.ts b/src/game/GameController.ts new file mode 100644 index 0000000..dec4463 --- /dev/null +++ b/src/game/GameController.ts @@ -0,0 +1,107 @@ +import { WindowManager } from './WindowManager.js'; +import { InputSender, VK } from './InputSender.js'; +import { sleep, randomDelay } from '../util/sleep.js'; +import { writeClipboard } from '../util/clipboard.js'; +import { logger } from '../util/logger.js'; +import type { Config } from '../types.js'; + +export class GameController { + private windowManager: WindowManager; + private inputSender: InputSender; + + constructor(config: Config) { + this.windowManager = new WindowManager(config.poe2WindowTitle); + this.inputSender = new InputSender(); + } + + async focusGame(): Promise { + const result = this.windowManager.focusWindow(); + if (result) { + await sleep(300); // Wait for window to actually focus + } + return result; + } + + isGameFocused(): boolean { + return this.windowManager.isGameFocused(); + } + + getWindowRect() { + return this.windowManager.getWindowRect(); + } + + async sendChat(message: string): Promise { + logger.info({ message }, 'Sending chat message'); + + // Open chat + await this.inputSender.pressKey(VK.RETURN); + await randomDelay(100, 200); + + // Clear any existing text + await this.inputSender.selectAll(); + await sleep(50); + await this.inputSender.pressKey(VK.DELETE); + await sleep(50); + + // Type the message + await this.inputSender.typeText(message); + await randomDelay(50, 100); + + // Send + await this.inputSender.pressKey(VK.RETURN); + await sleep(100); + } + + async sendChatViaPaste(message: string): Promise { + logger.info({ message }, 'Sending chat message via paste'); + + // Copy message to clipboard + writeClipboard(message); + await sleep(50); + + // Open chat + await this.inputSender.pressKey(VK.RETURN); + await randomDelay(100, 200); + + // Clear any existing text + await this.inputSender.selectAll(); + await sleep(50); + await this.inputSender.pressKey(VK.DELETE); + await sleep(50); + + // Paste + await this.inputSender.paste(); + await randomDelay(100, 200); + + // Send + await this.inputSender.pressKey(VK.RETURN); + await sleep(100); + } + + async goToHideout(): Promise { + logger.info('Sending /hideout command'); + await this.sendChat('/hideout'); + } + + async ctrlRightClickAt(x: number, y: number): Promise { + await this.inputSender.ctrlRightClick(x, y); + } + + async leftClickAt(x: number, y: number): Promise { + await this.inputSender.leftClick(x, y); + } + + async rightClickAt(x: number, y: number): Promise { + await this.inputSender.rightClick(x, y); + } + + async pressEscape(): Promise { + await this.inputSender.pressKey(VK.ESCAPE); + } + + async openInventory(): Promise { + logger.info('Opening inventory'); + await this.inputSender.pressKey(VK.I); + await sleep(300); + } +} diff --git a/src/game/InputSender.ts b/src/game/InputSender.ts new file mode 100644 index 0000000..585e18d --- /dev/null +++ b/src/game/InputSender.ts @@ -0,0 +1,294 @@ +import koffi from 'koffi'; +import { sleep, randomDelay } from '../util/sleep.js'; + +// Win32 POINT struct for GetCursorPos +const POINT = koffi.struct('POINT', { x: 'int32', y: 'int32' }); + +// Win32 INPUT struct on x64 is 40 bytes: +// type (4) + pad (4) + union (32) +// MOUSEINPUT is 32 bytes (the largest union member) +// KEYBDINPUT is 24 bytes, so needs 8 bytes trailing pad in the union +// +// We define flat structs that match the exact memory layout, +// then cast with koffi.as() when calling SendInput. + +const INPUT_KEYBOARD = koffi.struct('INPUT_KEYBOARD', { + type: 'uint32', // offset 0 + _pad0: 'uint32', // offset 4 (alignment for union at offset 8) + wVk: 'uint16', // offset 8 + wScan: 'uint16', // offset 10 + dwFlags: 'uint32', // offset 12 + time: 'uint32', // offset 16 + _pad1: 'uint32', // offset 20 (alignment for dwExtraInfo) + dwExtraInfo: 'uint64', // offset 24 + _pad2: koffi.array('uint8', 8), // offset 32, pad to 40 bytes total +}); + +const INPUT_MOUSE = koffi.struct('INPUT_MOUSE', { + type: 'uint32', // offset 0 + _pad0: 'uint32', // offset 4 (alignment for union at offset 8) + dx: 'int32', // offset 8 + dy: 'int32', // offset 12 + mouseData: 'uint32', // offset 16 + dwFlags: 'uint32', // offset 20 + time: 'uint32', // offset 24 + _pad1: 'uint32', // offset 28 (alignment for dwExtraInfo) + dwExtraInfo: 'uint64', // offset 32 +}); +// INPUT_MOUSE is already 40 bytes, no trailing pad needed + +const user32 = koffi.load('user32.dll'); + +const SendInput = user32.func('SendInput', 'uint32', ['uint32', 'void *', 'int32']); +const MapVirtualKeyW = user32.func('MapVirtualKeyW', 'uint32', ['uint32', 'uint32']); +const GetSystemMetrics = user32.func('GetSystemMetrics', 'int32', ['int32']); +const GetCursorPos = user32.func('GetCursorPos', 'int32', ['_Out_ POINT *']); + +// Constants +const INPUT_MOUSE_TYPE = 0; +const INPUT_KEYBOARD_TYPE = 1; +const KEYEVENTF_SCANCODE = 0x0008; +const KEYEVENTF_KEYUP = 0x0002; +const KEYEVENTF_UNICODE = 0x0004; + +// Mouse flags +const MOUSEEVENTF_MOVE = 0x0001; +const MOUSEEVENTF_LEFTDOWN = 0x0002; +const MOUSEEVENTF_LEFTUP = 0x0004; +const MOUSEEVENTF_RIGHTDOWN = 0x0008; +const MOUSEEVENTF_RIGHTUP = 0x0010; +const MOUSEEVENTF_ABSOLUTE = 0x8000; + +// System metrics +const SM_CXSCREEN = 0; +const SM_CYSCREEN = 1; + +// Virtual key codes +export const VK = { + RETURN: 0x0d, + CONTROL: 0x11, + MENU: 0x12, // Alt + SHIFT: 0x10, + ESCAPE: 0x1b, + TAB: 0x09, + SPACE: 0x20, + DELETE: 0x2e, + BACK: 0x08, + V: 0x56, + A: 0x41, + C: 0x43, + I: 0x49, +} as const; + +// Size to pass to SendInput (must be sizeof(INPUT) = 40 on x64) +const INPUT_SIZE = koffi.sizeof(INPUT_MOUSE); // 40 + +// Bézier curve helpers for natural mouse movement + +function easeInOutQuad(t: number): number { + return t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2; +} + +interface Point { + x: number; + y: number; +} + +function cubicBezier(t: number, p0: Point, p1: Point, p2: Point, p3: Point): Point { + const u = 1 - t; + const u2 = u * u; + const u3 = u2 * u; + const t2 = t * t; + const t3 = t2 * t; + return { + x: u3 * p0.x + 3 * u2 * t * p1.x + 3 * u * t2 * p2.x + t3 * p3.x, + y: u3 * p0.y + 3 * u2 * t * p1.y + 3 * u * t2 * p2.y + t3 * p3.y, + }; +} + +function clamp(value: number, min: number, max: number): number { + return Math.max(min, Math.min(max, value)); +} + +export class InputSender { + private screenWidth: number; + private screenHeight: number; + + constructor() { + this.screenWidth = GetSystemMetrics(SM_CXSCREEN); + this.screenHeight = GetSystemMetrics(SM_CYSCREEN); + } + + async pressKey(vkCode: number): Promise { + const scanCode = MapVirtualKeyW(vkCode, 0); // MAPVK_VK_TO_VSC + this.sendScanKeyDown(scanCode); + await randomDelay(30, 50); + this.sendScanKeyUp(scanCode); + await randomDelay(20, 40); + } + + async keyDown(vkCode: number): Promise { + const scanCode = MapVirtualKeyW(vkCode, 0); + this.sendScanKeyDown(scanCode); + await randomDelay(15, 30); + } + + async keyUp(vkCode: number): Promise { + const scanCode = MapVirtualKeyW(vkCode, 0); + this.sendScanKeyUp(scanCode); + await randomDelay(15, 30); + } + + async typeText(text: string): Promise { + for (const char of text) { + this.sendUnicodeChar(char); + await randomDelay(20, 50); + } + } + + async paste(): Promise { + await this.keyDown(VK.CONTROL); + await sleep(30); + await this.pressKey(VK.V); + await this.keyUp(VK.CONTROL); + await sleep(50); + } + + async selectAll(): Promise { + await this.keyDown(VK.CONTROL); + await sleep(30); + await this.pressKey(VK.A); + await this.keyUp(VK.CONTROL); + await sleep(50); + } + + private getCursorPos(): Point { + const pt = { x: 0, y: 0 }; + GetCursorPos(pt); + return pt; + } + + private moveMouseRaw(x: number, y: number): void { + const normalizedX = Math.round((x * 65535) / this.screenWidth); + const normalizedY = Math.round((y * 65535) / this.screenHeight); + this.sendMouseInput(normalizedX, normalizedY, 0, MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE); + } + + async moveMouse(x: number, y: number): Promise { + const start = this.getCursorPos(); + const end: Point = { x, y }; + const dx = end.x - start.x; + const dy = end.y - start.y; + const distance = Math.sqrt(dx * dx + dy * dy); + + // Short distance: just teleport + if (distance < 10) { + this.moveMouseRaw(x, y); + await randomDelay(10, 20); + return; + } + + // Generate 2 random control points offset from the straight line + const perpX = -dy / distance; + const perpY = dx / distance; + const spread = distance * 0.3; + + const cp1: Point = { + x: start.x + dx * 0.25 + perpX * (Math.random() - 0.5) * spread, + y: start.y + dy * 0.25 + perpY * (Math.random() - 0.5) * spread, + }; + const cp2: Point = { + x: start.x + dx * 0.75 + perpX * (Math.random() - 0.5) * spread, + y: start.y + dy * 0.75 + perpY * (Math.random() - 0.5) * spread, + }; + + const steps = clamp(Math.round(distance / 15), 15, 40); + + for (let i = 1; i <= steps; i++) { + const rawT = i / steps; + const t = easeInOutQuad(rawT); + const pt = cubicBezier(t, start, cp1, cp2, end); + + // Add ±1px jitter except on the last step + const jitterX = i < steps ? Math.round((Math.random() - 0.5) * 2) : 0; + const jitterY = i < steps ? Math.round((Math.random() - 0.5) * 2) : 0; + + this.moveMouseRaw(Math.round(pt.x) + jitterX, Math.round(pt.y) + jitterY); + await sleep(2 + Math.random() * 3); // 2-5ms between steps + } + + // Final exact landing + this.moveMouseRaw(x, y); + await randomDelay(10, 25); + } + + async leftClick(x: number, y: number): Promise { + await this.moveMouse(x, y); + await randomDelay(50, 100); + this.sendMouseInput(0, 0, 0, MOUSEEVENTF_LEFTDOWN); + await randomDelay(30, 80); + this.sendMouseInput(0, 0, 0, MOUSEEVENTF_LEFTUP); + await randomDelay(30, 60); + } + + async rightClick(x: number, y: number): Promise { + await this.moveMouse(x, y); + await randomDelay(50, 100); + this.sendMouseInput(0, 0, 0, MOUSEEVENTF_RIGHTDOWN); + await randomDelay(30, 80); + this.sendMouseInput(0, 0, 0, MOUSEEVENTF_RIGHTUP); + await randomDelay(30, 60); + } + + async ctrlRightClick(x: number, y: number): Promise { + await this.keyDown(VK.CONTROL); + await randomDelay(30, 60); + await this.rightClick(x, y); + await this.keyUp(VK.CONTROL); + await randomDelay(30, 60); + } + + private sendMouseInput(dx: number, dy: number, mouseData: number, flags: number): void { + const input = { + type: INPUT_MOUSE_TYPE, + _pad0: 0, + dx, + dy, + mouseData, + dwFlags: flags, + time: 0, + _pad1: 0, + dwExtraInfo: 0, + }; + SendInput(1, koffi.as(input, 'INPUT_MOUSE *'), INPUT_SIZE); + } + + private sendKeyInput(wVk: number, wScan: number, flags: number): void { + const input = { + type: INPUT_KEYBOARD_TYPE, + _pad0: 0, + wVk, + wScan, + dwFlags: flags, + time: 0, + _pad1: 0, + dwExtraInfo: 0, + _pad2: [0, 0, 0, 0, 0, 0, 0, 0], + }; + SendInput(1, koffi.as(input, 'INPUT_KEYBOARD *'), INPUT_SIZE); + } + + private sendScanKeyDown(scanCode: number): void { + this.sendKeyInput(0, scanCode, KEYEVENTF_SCANCODE); + } + + private sendScanKeyUp(scanCode: number): void { + this.sendKeyInput(0, scanCode, KEYEVENTF_SCANCODE | KEYEVENTF_KEYUP); + } + + private sendUnicodeChar(char: string): void { + const code = char.charCodeAt(0); + this.sendKeyInput(0, code, KEYEVENTF_UNICODE); + this.sendKeyInput(0, code, KEYEVENTF_UNICODE | KEYEVENTF_KEYUP); + } +} diff --git a/src/game/OcrDaemon.ts b/src/game/OcrDaemon.ts new file mode 100644 index 0000000..256e7b5 --- /dev/null +++ b/src/game/OcrDaemon.ts @@ -0,0 +1,256 @@ +import { spawn, type ChildProcess } from 'child_process'; +import { join } from 'path'; +import { logger } from '../util/logger.js'; +import type { Region } from '../types.js'; + +// ── Types ─────────────────────────────────────────────────────────────────── + +export interface OcrWord { + text: string; + x: number; + y: number; + width: number; + height: number; +} + +export interface OcrLine { + text: string; + words: OcrWord[]; +} + +export interface OcrResponse { + ok: true; + text: string; + lines: OcrLine[]; +} + +interface DaemonRequest { + cmd: string; + region?: Region; + path?: string; +} + +interface DaemonResponse { + ok: boolean; + ready?: boolean; + text?: string; + lines?: OcrLine[]; + image?: string; + error?: string; +} + +// ── OcrDaemon ─────────────────────────────────────────────────────────────── + +const DEFAULT_EXE = join( + 'tools', 'OcrDaemon', 'bin', 'Release', + 'net8.0-windows10.0.19041.0', 'OcrDaemon.exe', +); + +const REQUEST_TIMEOUT = 5_000; +const CAPTURE_TIMEOUT = 10_000; + +export class OcrDaemon { + private proc: ChildProcess | null = null; + private exePath: string; + private readyResolve: ((value: void) => void) | null = null; + private readyReject: ((err: Error) => void) | null = null; + private pendingResolve: ((resp: DaemonResponse) => void) | null = null; + private pendingReject: ((err: Error) => void) | null = null; + private queue: Array<{ request: DaemonRequest; resolve: (resp: DaemonResponse) => void; reject: (err: Error) => void }> = []; + private processing = false; + private buffer = ''; + private stopped = false; + + constructor(exePath?: string) { + this.exePath = exePath ?? DEFAULT_EXE; + } + + // ── Public API ────────────────────────────────────────────────────────── + + async ocr(region?: Region): Promise { + const req: DaemonRequest = { cmd: 'ocr' }; + if (region) req.region = region; + const resp = await this.sendWithRetry(req, REQUEST_TIMEOUT); + return { + ok: true, + text: resp.text ?? '', + lines: resp.lines ?? [], + }; + } + + async captureBuffer(region?: Region): Promise { + const req: DaemonRequest = { cmd: 'capture' }; + if (region) req.region = region; + const resp = await this.sendWithRetry(req, CAPTURE_TIMEOUT); + return Buffer.from(resp.image!, 'base64'); + } + + async saveScreenshot(path: string, region?: Region): Promise { + const req: DaemonRequest = { cmd: 'screenshot', path }; + if (region) req.region = region; + await this.sendWithRetry(req, REQUEST_TIMEOUT); + } + + async stop(): Promise { + this.stopped = true; + if (this.proc) { + const p = this.proc; + this.proc = null; + p.stdin?.end(); + p.kill(); + } + } + + // ── Internal ──────────────────────────────────────────────────────────── + + private async ensureRunning(): Promise { + if (this.proc && this.proc.exitCode === null) return; + + this.proc = null; + this.buffer = ''; + + logger.info({ exe: this.exePath }, 'Spawning OCR daemon'); + + const proc = spawn(this.exePath, [], { + stdio: ['pipe', 'pipe', 'pipe'], + windowsHide: true, + }); + + this.proc = proc; + + proc.stderr?.on('data', (data: Buffer) => { + logger.warn({ daemon: data.toString().trim() }, 'OcrDaemon stderr'); + }); + + proc.on('exit', (code) => { + logger.warn({ code }, 'OcrDaemon exited'); + if (this.pendingReject) { + this.pendingReject(new Error(`Daemon exited with code ${code}`)); + this.pendingResolve = null; + this.pendingReject = null; + } + }); + + proc.stdout!.on('data', (data: Buffer) => { + this.buffer += data.toString(); + this.processBuffer(); + }); + + // Wait for ready signal + await new Promise((resolve, reject) => { + this.readyResolve = resolve; + this.readyReject = reject; + + const timeout = setTimeout(() => { + this.readyReject = null; + this.readyResolve = null; + reject(new Error('Daemon did not become ready within 10s')); + }, 10_000); + + // Store so we can clear on resolve + (this as any)._readyTimeout = timeout; + }); + + logger.info('OCR daemon ready'); + } + + private processBuffer(): void { + let newlineIdx: number; + while ((newlineIdx = this.buffer.indexOf('\n')) !== -1) { + const line = this.buffer.slice(0, newlineIdx).trim(); + this.buffer = this.buffer.slice(newlineIdx + 1); + + if (!line) continue; + + let parsed: DaemonResponse; + try { + parsed = JSON.parse(line); + } catch { + logger.warn({ line }, 'Failed to parse daemon response'); + continue; + } + + // Handle ready signal + if (parsed.ready && this.readyResolve) { + clearTimeout((this as any)._readyTimeout); + const resolve = this.readyResolve; + this.readyResolve = null; + this.readyReject = null; + resolve(); + continue; + } + + // Handle normal response + if (this.pendingResolve) { + const resolve = this.pendingResolve; + this.pendingResolve = null; + this.pendingReject = null; + resolve(parsed); + } + } + } + + private async send(request: DaemonRequest, timeout: number): Promise { + await this.ensureRunning(); + + return new Promise((resolve, reject) => { + this.queue.push({ request, resolve, reject }); + this.drainQueue(timeout); + }); + } + + private drainQueue(timeout: number): void { + if (this.processing || this.queue.length === 0) return; + this.processing = true; + + const { request, resolve, reject } = this.queue.shift()!; + + const timer = setTimeout(() => { + this.pendingResolve = null; + this.pendingReject = null; + this.processing = false; + reject(new Error(`Daemon request timed out after ${timeout}ms`)); + this.drainQueue(timeout); + }, timeout); + + this.pendingResolve = (resp) => { + clearTimeout(timer); + this.processing = false; + resolve(resp); + this.drainQueue(timeout); + }; + + this.pendingReject = (err) => { + clearTimeout(timer); + this.processing = false; + reject(err); + this.drainQueue(timeout); + }; + + const json = JSON.stringify(request) + '\n'; + this.proc!.stdin!.write(json); + } + + private async sendWithRetry(request: DaemonRequest, timeout: number): Promise { + try { + const resp = await this.send(request, timeout); + if (!resp.ok) throw new Error(resp.error ?? 'Daemon returned error'); + return resp; + } catch (err) { + if (this.stopped) throw err; + + // Kill and retry once + logger.warn({ err, cmd: request.cmd }, 'Daemon request failed, restarting'); + if (this.proc) { + const p = this.proc; + this.proc = null; + p.stdin?.end(); + p.kill(); + } + + const resp = await this.send(request, timeout); + if (!resp.ok) throw new Error(resp.error ?? 'Daemon returned error on retry'); + return resp; + } + } +} diff --git a/src/game/ScreenReader.ts b/src/game/ScreenReader.ts new file mode 100644 index 0000000..bc34672 --- /dev/null +++ b/src/game/ScreenReader.ts @@ -0,0 +1,129 @@ +import { mkdir } from 'fs/promises'; +import { join } from 'path'; +import { logger } from '../util/logger.js'; +import { OcrDaemon, type OcrResponse } from './OcrDaemon.js'; +import type { Region } from '../types.js'; + +function elapsed(start: number): string { + return `${(performance.now() - start).toFixed(0)}ms`; +} + +export class ScreenReader { + private daemon = new OcrDaemon(); + + // ── Screenshot capture ────────────────────────────────────────────── + + async captureScreen(): Promise { + const t = performance.now(); + const buf = await this.daemon.captureBuffer(); + logger.info({ ms: elapsed(t) }, 'captureScreen'); + return buf; + } + + async captureRegion(region: Region): Promise { + const t = performance.now(); + const buf = await this.daemon.captureBuffer(region); + logger.info({ ms: elapsed(t) }, 'captureRegion'); + return buf; + } + + // ── OCR helpers ───────────────────────────────────────────────────── + + private findWordInOcrResult( + result: OcrResponse, + needle: string, + ): { x: number; y: number } | null { + const lower = needle.toLowerCase(); + for (const line of result.lines) { + for (const word of line.words) { + if (word.text.toLowerCase().includes(lower)) { + return { + x: Math.round(word.x + word.width / 2), + y: Math.round(word.y + word.height / 2), + }; + } + } + } + return null; + } + + // ── Full-screen methods ───────────────────────────────────────────── + + async findTextOnScreen( + searchText: string, + ): Promise<{ x: number; y: number } | null> { + const t = performance.now(); + const result = await this.daemon.ocr(); + const pos = this.findWordInOcrResult(result, searchText); + + if (pos) { + logger.info({ searchText, x: pos.x, y: pos.y, totalMs: elapsed(t) }, 'Found text on screen'); + } else { + logger.info({ searchText, totalMs: elapsed(t) }, 'Text not found on screen'); + } + return pos; + } + + async readFullScreen(): Promise { + const result = await this.daemon.ocr(); + return result.text; + } + + // ── Region methods ────────────────────────────────────────────────── + + async findTextInRegion( + region: Region, + searchText: string, + ): Promise<{ x: number; y: number } | null> { + const t = performance.now(); + const result = await this.daemon.ocr(region); + const pos = this.findWordInOcrResult(result, searchText); + + if (pos) { + // Offset back to screen space + const screenPos = { x: region.x + pos.x, y: region.y + pos.y }; + logger.info({ searchText, x: screenPos.x, y: screenPos.y, region, totalMs: elapsed(t) }, 'Found text in region'); + return screenPos; + } + + logger.info({ searchText, region, totalMs: elapsed(t) }, 'Text not found in region'); + return null; + } + + async readRegionText(region: Region): Promise { + const result = await this.daemon.ocr(region); + return result.text; + } + + async checkForText(region: Region, searchText: string): Promise { + const pos = await this.findTextInRegion(region, searchText); + return pos !== null; + } + + // ── Save utilities ────────────────────────────────────────────────── + + async saveScreenshot(path: string): Promise { + await this.daemon.saveScreenshot(path); + logger.info({ path }, 'Screenshot saved'); + } + + async saveDebugScreenshots(dir: string): Promise { + await mkdir(dir, { recursive: true }); + const ts = Date.now(); + const originalPath = join(dir, `${ts}-screenshot.png`); + await this.daemon.saveScreenshot(originalPath); + logger.info({ dir, files: [originalPath.split(/[\\/]/).pop()] }, 'Debug screenshot saved'); + return [originalPath]; + } + + async saveRegion(region: Region, path: string): Promise { + await this.daemon.saveScreenshot(path, region); + logger.info({ path, region }, 'Region screenshot saved'); + } + + // ── Lifecycle ─────────────────────────────────────────────────────── + + async dispose(): Promise { + await this.daemon.stop(); + } +} diff --git a/src/game/WindowManager.ts b/src/game/WindowManager.ts new file mode 100644 index 0000000..7f3b083 --- /dev/null +++ b/src/game/WindowManager.ts @@ -0,0 +1,90 @@ +import koffi from 'koffi'; +import { logger } from '../util/logger.js'; + +// Win32 types +const HWND = 'int'; +const BOOL = 'bool'; +const RECT = koffi.struct('RECT', { + left: 'long', + top: 'long', + right: 'long', + bottom: 'long', +}); + +// Load user32.dll +const user32 = koffi.load('user32.dll'); + +const FindWindowW = user32.func('FindWindowW', HWND, ['str16', 'str16']); +const SetForegroundWindow = user32.func('SetForegroundWindow', BOOL, [HWND]); +const ShowWindow = user32.func('ShowWindow', BOOL, [HWND, 'int']); +const BringWindowToTop = user32.func('BringWindowToTop', BOOL, [HWND]); +const GetForegroundWindow = user32.func('GetForegroundWindow', HWND, []); +const GetWindowRect = user32.func('GetWindowRect', BOOL, [HWND, koffi.out(koffi.pointer(RECT))]); +const IsWindow = user32.func('IsWindow', BOOL, [HWND]); +const keybd_event = user32.func('keybd_event', 'void', ['uint8', 'uint8', 'uint32', 'uint']); +const MapVirtualKeyW = user32.func('MapVirtualKeyW', 'uint32', ['uint32', 'uint32']); + +// Constants +const SW_RESTORE = 9; +const VK_MENU = 0x12; // Alt key +const KEYEVENTF_KEYUP = 0x0002; + +export class WindowManager { + private hwnd: number = 0; + + constructor(private windowTitle: string) {} + + findWindow(): number { + this.hwnd = FindWindowW(null as unknown as string, this.windowTitle); + if (this.hwnd === 0) { + logger.warn({ title: this.windowTitle }, 'Window not found'); + } else { + logger.info({ title: this.windowTitle, hwnd: this.hwnd }, 'Window found'); + } + return this.hwnd; + } + + focusWindow(): boolean { + if (!this.hwnd || !IsWindow(this.hwnd)) { + this.findWindow(); + } + if (!this.hwnd) return false; + + // Restore if minimized + ShowWindow(this.hwnd, SW_RESTORE); + + // Alt-key trick to bypass SetForegroundWindow restriction + const altScan = MapVirtualKeyW(VK_MENU, 0); + keybd_event(VK_MENU, altScan, 0, 0); + keybd_event(VK_MENU, altScan, KEYEVENTF_KEYUP, 0); + + BringWindowToTop(this.hwnd); + const result = SetForegroundWindow(this.hwnd); + + if (!result) { + logger.warn('SetForegroundWindow failed'); + } + return result; + } + + getWindowRect(): { left: number; top: number; right: number; bottom: number } | null { + if (!this.hwnd || !IsWindow(this.hwnd)) { + this.findWindow(); + } + if (!this.hwnd) return null; + + const rect = { left: 0, top: 0, right: 0, bottom: 0 }; + const success = GetWindowRect(this.hwnd, rect); + if (!success) return null; + return rect; + } + + isGameFocused(): boolean { + const fg = GetForegroundWindow(); + return fg === this.hwnd && this.hwnd !== 0; + } + + getHwnd(): number { + return this.hwnd; + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..01990d2 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,190 @@ +import { Command } from 'commander'; +import { loadConfig } from './config.js'; +import { TradeMonitor } from './trade/TradeMonitor.js'; +import { GameController } from './game/GameController.js'; +import { ScreenReader } from './game/ScreenReader.js'; +import { ClientLogWatcher } from './log/ClientLogWatcher.js'; +import { TradeExecutor } from './executor/TradeExecutor.js'; +import { TradeQueue } from './executor/TradeQueue.js'; +import { BotController } from './dashboard/BotController.js'; +import { DashboardServer } from './dashboard/DashboardServer.js'; +import { ConfigStore } from './dashboard/ConfigStore.js'; +import { logger } from './util/logger.js'; +import type { Page } from 'playwright'; + +const program = new Command(); + +program + .name('poe2trade') + .description('POE2 automated trade bot') + .option('-u, --url ', 'Trade search URLs to monitor') + .option('--log-path ', 'Path to POE2 Client.txt') + .option('-p, --port ', 'Dashboard port') + .option('-c, --config ', 'Path to config.json', 'config.json') + .action(async (options) => { + // Load persisted config + const store = new ConfigStore(options.config); + const saved = store.settings; + + // CLI/env overrides persisted values + const envConfig = loadConfig(options.url); + if (options.logPath) envConfig.poe2LogPath = options.logPath; + + // Merge: CLI args > .env > config.json defaults + const config = { + ...envConfig, + poe2LogPath: options.logPath || saved.poe2LogPath, + poe2WindowTitle: saved.poe2WindowTitle, + browserUserDataDir: saved.browserUserDataDir, + travelTimeoutMs: saved.travelTimeoutMs, + stashScanTimeoutMs: saved.stashScanTimeoutMs, + waitForMoreItemsMs: saved.waitForMoreItemsMs, + betweenTradesDelayMs: saved.betweenTradesDelayMs, + }; + + const port = parseInt(options.port, 10) || saved.dashboardPort; + + // Collect all URLs: CLI args + saved links (deduped) + const allUrls = new Set([ + ...config.tradeUrls, + ...saved.links.map((l) => l.url), + ]); + + // Initialize bot controller with config store + const bot = new BotController(store); + + // Start dashboard + const dashboard = new DashboardServer(bot, port); + await dashboard.start(); + + // Initialize game components + const screenReader = new ScreenReader(); + + const gameController = new GameController(config); + dashboard.setDebugDeps({ screenReader, gameController }); + + const logWatcher = new ClientLogWatcher(config.poe2LogPath); + await logWatcher.start(); + dashboard.broadcastLog('info', 'Watching Client.txt for game events'); + + const tradeMonitor = new TradeMonitor(config); + await tradeMonitor.start(`http://localhost:${port}`); + dashboard.broadcastLog('info', 'Browser launched'); + + const executor = new TradeExecutor( + gameController, + screenReader, + logWatcher, + tradeMonitor, + config, + ); + + const tradeQueue = new TradeQueue(executor, config); + + // Helper to add a trade search + const activateLink = async (url: string) => { + try { + await tradeMonitor.addSearch(url); + dashboard.broadcastLog('info', `Monitoring: ${url}`); + dashboard.broadcastStatus(); + } catch (err) { + logger.error({ err, url }, 'Failed to add trade search'); + dashboard.broadcastLog('error', `Failed to add: ${url}`); + } + }; + + // Load all saved + CLI links (only activate ones marked active) + for (const url of allUrls) { + const link = bot.addLink(url); + if (link.active) { + await activateLink(url); + } else { + dashboard.broadcastLog('info', `Loaded (inactive): ${link.name || link.label}`); + } + } + + dashboard.broadcastLog('info', `Loaded ${allUrls.size} trade link(s) from config`); + + // When dashboard adds a link, activate it in the trade monitor + bot.on('link-added', async (link) => { + if (link.active) { + await activateLink(link.url); + } + }); + + // When dashboard removes a link, deactivate it + bot.on('link-removed', async (id: string) => { + await tradeMonitor.removeSearch(id); + dashboard.broadcastLog('info', `Removed search: ${id}`); + dashboard.broadcastStatus(); + }); + + // When dashboard toggles a link active/inactive + bot.on('link-toggled', async (data: { id: string; active: boolean; link: { url: string; name: string } }) => { + if (data.active) { + await activateLink(data.link.url); + dashboard.broadcastLog('info', `Activated: ${data.link.name || data.id}`); + } else { + await tradeMonitor.pauseSearch(data.id); + dashboard.broadcastLog('info', `Deactivated: ${data.link.name || data.id}`); + } + }); + + // Wire up events: when new listings appear, queue them for trading + tradeMonitor.on('new-listings', (data: { searchId: string; itemIds: string[]; page: Page }) => { + if (bot.isPaused) { + dashboard.broadcastLog('warn', `New listings (${data.itemIds.length}) skipped - bot paused`); + return; + } + + // Check if this specific link is active + if (!bot.isLinkActive(data.searchId)) { + return; + } + + logger.info( + { searchId: data.searchId, itemCount: data.itemIds.length }, + 'New listings received, queuing trade...', + ); + dashboard.broadcastLog('info', `New listings: ${data.itemIds.length} items from ${data.searchId}`); + + tradeQueue.enqueue({ + searchId: data.searchId, + itemIds: data.itemIds, + whisperText: '', + timestamp: Date.now(), + tradeUrl: '', + page: data.page, + }); + }); + + // Forward executor state changes to dashboard + const stateInterval = setInterval(() => { + const execState = executor.getState(); + if (bot.state !== execState) { + bot.state = execState; + dashboard.broadcastStatus(); + } + }, 500); + + // Graceful shutdown + const shutdown = async () => { + logger.info('Shutting down...'); + clearInterval(stateInterval); + await screenReader.dispose(); + await dashboard.stop(); + await tradeMonitor.stop(); + await logWatcher.stop(); + process.exit(0); + }; + + process.on('SIGINT', shutdown); + process.on('SIGTERM', shutdown); + + logger.info(`Dashboard: http://localhost:${port}`); + logger.info( + `Monitoring ${allUrls.size} trade search(es). Press Ctrl+C to stop.`, + ); + }); + +program.parse(); diff --git a/src/log/ClientLogWatcher.ts b/src/log/ClientLogWatcher.ts new file mode 100644 index 0000000..7a3f220 --- /dev/null +++ b/src/log/ClientLogWatcher.ts @@ -0,0 +1,130 @@ +import { EventEmitter } from 'events'; +import { watch } from 'chokidar'; +import { createReadStream, statSync } from 'fs'; +import { createInterface } from 'readline'; +import { logger } from '../util/logger.js'; + +export interface LogEvents { + 'area-entered': (area: string) => void; + 'whisper-received': (data: { player: string; message: string }) => void; + 'whisper-sent': (data: { player: string; message: string }) => void; + 'trade-accepted': () => void; + 'party-joined': (player: string) => void; + 'party-left': (player: string) => void; + line: (line: string) => void; +} + +export class ClientLogWatcher extends EventEmitter { + private watcher: ReturnType | null = null; + private fileOffset: number = 0; + private logPath: string; + + constructor(logPath: string) { + super(); + this.logPath = logPath; + } + + async start(): Promise { + // Start reading from end of file (only new lines) + try { + const stats = statSync(this.logPath); + this.fileOffset = stats.size; + } catch { + logger.warn({ path: this.logPath }, 'Log file not found yet, will watch for creation'); + this.fileOffset = 0; + } + + this.watcher = watch(this.logPath, { + persistent: true, + usePolling: true, + interval: 200, + }); + + this.watcher.on('change', () => { + this.readNewLines(); + }); + + logger.info({ path: this.logPath }, 'Watching Client.txt for game events'); + } + + private readNewLines(): void { + const stream = createReadStream(this.logPath, { + start: this.fileOffset, + encoding: 'utf-8', + }); + + const rl = createInterface({ input: stream }); + let bytesRead = 0; + + rl.on('line', (line) => { + bytesRead += Buffer.byteLength(line, 'utf-8') + 2; // +2 for \r\n on Windows + if (line.trim()) { + this.parseLine(line.trim()); + } + }); + + rl.on('close', () => { + this.fileOffset += bytesRead; + }); + } + + private parseLine(line: string): void { + this.emit('line', line); + + // Area transition: "You have entered Hideout" + const areaMatch = line.match(/You have entered (.+?)\.?$/); + if (areaMatch) { + const area = areaMatch[1]; + logger.info({ area }, 'Area entered'); + this.emit('area-entered', area); + return; + } + + // Incoming whisper: "@From PlayerName: message" + const whisperFromMatch = line.match(/@From\s+(.+?):\s+(.+)$/); + if (whisperFromMatch) { + const data = { player: whisperFromMatch[1], message: whisperFromMatch[2] }; + logger.info(data, 'Whisper received'); + this.emit('whisper-received', data); + return; + } + + // Outgoing whisper: "@To PlayerName: message" + const whisperToMatch = line.match(/@To\s+(.+?):\s+(.+)$/); + if (whisperToMatch) { + const data = { player: whisperToMatch[1], message: whisperToMatch[2] }; + this.emit('whisper-sent', data); + return; + } + + // Party join: "PlayerName has joined the party" + const partyJoinMatch = line.match(/(.+?) has joined the party/); + if (partyJoinMatch) { + logger.info({ player: partyJoinMatch[1] }, 'Player joined party'); + this.emit('party-joined', partyJoinMatch[1]); + return; + } + + // Party leave: "PlayerName has left the party" + const partyLeaveMatch = line.match(/(.+?) has left the party/); + if (partyLeaveMatch) { + this.emit('party-left', partyLeaveMatch[1]); + return; + } + + // Trade accepted + if (line.includes('Trade accepted') || line.includes('Trade completed')) { + logger.info('Trade accepted/completed'); + this.emit('trade-accepted'); + return; + } + } + + async stop(): Promise { + if (this.watcher) { + await this.watcher.close(); + this.watcher = null; + } + logger.info('Client log watcher stopped'); + } +} diff --git a/src/trade/TradeMonitor.ts b/src/trade/TradeMonitor.ts new file mode 100644 index 0000000..3cf555b --- /dev/null +++ b/src/trade/TradeMonitor.ts @@ -0,0 +1,256 @@ +import { EventEmitter } from 'events'; +import { chromium, type Browser, type BrowserContext, type Page, type WebSocket } from 'playwright'; +import { SELECTORS } from './selectors.js'; +import { logger } from '../util/logger.js'; +import { sleep } from '../util/sleep.js'; +import type { Config } from '../types.js'; + +// Stealth JS injected into every page to avoid Playwright detection +const STEALTH_SCRIPT = ` + // Remove navigator.webdriver flag + Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); + + // Fake plugins array (empty = headless giveaway) + Object.defineProperty(navigator, 'plugins', { + get: () => [ + { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer' }, + { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai' }, + { name: 'Native Client', filename: 'internal-nacl-plugin' }, + ], + }); + + // Fake languages + Object.defineProperty(navigator, 'languages', { + get: () => ['en-US', 'en'], + }); + + // Remove Playwright/automation artifacts from window + delete window.__playwright; + delete window.__pw_manual; + + // Fix chrome.runtime to look like a real browser + if (!window.chrome) window.chrome = {}; + if (!window.chrome.runtime) window.chrome.runtime = { id: undefined }; + + // Prevent detection via permissions API + const originalQuery = window.navigator.permissions?.query; + if (originalQuery) { + window.navigator.permissions.query = (params) => { + if (params.name === 'notifications') { + return Promise.resolve({ state: Notification.permission }); + } + return originalQuery(params); + }; + } +`; + +export class TradeMonitor extends EventEmitter { + private browser: Browser | null = null; + private context: BrowserContext | null = null; + private pages: Map = new Map(); + private pausedSearches: Set = new Set(); + private config: Config; + + constructor(config: Config) { + super(); + this.config = config; + } + + async start(dashboardUrl?: string): Promise { + logger.info('Launching Playwright browser (stealth mode)...'); + + this.context = await chromium.launchPersistentContext(this.config.browserUserDataDir, { + headless: false, + viewport: null, + args: [ + '--disable-blink-features=AutomationControlled', + '--disable-features=AutomationControlled', + '--no-first-run', + '--no-default-browser-check', + '--disable-infobars', + ], + ignoreDefaultArgs: ['--enable-automation'], + }); + + // Inject stealth script into all pages (current and future) + await this.context.addInitScript(STEALTH_SCRIPT); + + // Open dashboard as the first tab + if (dashboardUrl) { + const pages = this.context.pages(); + if (pages.length > 0) { + await pages[0].goto(dashboardUrl); + } else { + const page = await this.context.newPage(); + await page.goto(dashboardUrl); + } + logger.info({ dashboardUrl }, 'Dashboard opened in browser'); + } + + logger.info('Browser launched (stealth active).'); + } + + async addSearch(tradeUrl: string): Promise { + if (!this.context) throw new Error('Browser not started'); + + const searchId = this.extractSearchId(tradeUrl); + + // Don't add duplicate + if (this.pages.has(searchId)) { + logger.info({ searchId }, 'Search already open, skipping'); + return; + } + + logger.info({ tradeUrl, searchId }, 'Adding trade search'); + + const page = await this.context.newPage(); + this.pages.set(searchId, page); + + await page.goto(tradeUrl, { waitUntil: 'networkidle' }); + await sleep(2000); + + // Listen for WebSocket connections (must be registered before clicking live search) + page.on('websocket', (ws: WebSocket) => { + this.handleWebSocket(ws, searchId, page); + }); + + // Click the "Activate Live Search" button + try { + const liveBtn = page.locator(SELECTORS.liveSearchButton).first(); + await liveBtn.click({ timeout: 5000 }); + logger.info({ searchId }, 'Live search activated'); + } catch { + logger.warn({ searchId }, 'Could not click Activate Live Search button'); + } + + logger.info({ searchId }, 'Trade search monitoring active'); + } + + async pauseSearch(searchId: string): Promise { + this.pausedSearches.add(searchId); + // Close the page to stop the WebSocket / live search + const page = this.pages.get(searchId); + if (page) { + await page.close(); + this.pages.delete(searchId); + } + logger.info({ searchId }, 'Search paused (page closed)'); + } + + async resumeSearch(tradeUrl: string): Promise { + const searchId = this.extractSearchId(tradeUrl); + this.pausedSearches.delete(searchId); + await this.addSearch(tradeUrl); + logger.info({ searchId }, 'Search resumed'); + } + + isSearchActive(searchId: string): boolean { + return this.pages.has(searchId) && !this.pausedSearches.has(searchId); + } + + private handleWebSocket(ws: WebSocket, searchId: string, page: Page): void { + const url = ws.url(); + + if (!url.includes('/api/trade') || !url.includes('/live/')) { + return; + } + + logger.info({ url, searchId }, 'WebSocket connected for live search'); + + ws.on('framereceived', (frame) => { + // Don't emit if this search is paused + if (this.pausedSearches.has(searchId)) return; + + try { + const payload = typeof frame.payload === 'string' ? frame.payload : frame.payload.toString(); + const data = JSON.parse(payload); + + if (data.new && Array.isArray(data.new) && data.new.length > 0) { + logger.info({ searchId, itemCount: data.new.length, itemIds: data.new }, 'New listings detected!'); + this.emit('new-listings', { + searchId, + itemIds: data.new as string[], + page, + }); + } + } catch { + // Not all frames are JSON + } + }); + + ws.on('close', () => { + logger.warn({ searchId }, 'WebSocket closed'); + }); + + ws.on('socketerror', (err) => { + logger.error({ searchId, err }, 'WebSocket error'); + }); + } + + async clickTravelToHideout(page: Page, itemId?: string): Promise { + try { + if (itemId) { + const row = page.locator(SELECTORS.listingById(itemId)); + if (await row.isVisible({ timeout: 5000 })) { + const travelBtn = row.locator(SELECTORS.travelToHideoutButton).first(); + if (await travelBtn.isVisible({ timeout: 3000 })) { + await travelBtn.click(); + logger.info({ itemId }, 'Clicked Travel to Hideout for specific item'); + await this.handleConfirmDialog(page); + return true; + } + } + } + + const travelBtn = page.locator(SELECTORS.travelToHideoutButton).first(); + await travelBtn.click({ timeout: 5000 }); + logger.info('Clicked Travel to Hideout'); + await this.handleConfirmDialog(page); + return true; + } catch (err) { + logger.error({ err }, 'Failed to click Travel to Hideout'); + return false; + } + } + + private async handleConfirmDialog(page: Page): Promise { + await sleep(500); + try { + const confirmBtn = page.locator(SELECTORS.confirmYesButton).first(); + if (await confirmBtn.isVisible({ timeout: 2000 })) { + await confirmBtn.click(); + logger.info('Confirmed "Are you sure?" dialog'); + } + } catch { + // No dialog + } + } + + extractSearchId(url: string): string { + const cleaned = url.replace(/\/live\/?$/, ''); + const parts = cleaned.split('/'); + return parts[parts.length - 1] || url; + } + + async removeSearch(searchId: string): Promise { + this.pausedSearches.delete(searchId); + const page = this.pages.get(searchId); + if (page) { + await page.close(); + this.pages.delete(searchId); + logger.info({ searchId }, 'Trade search removed'); + } + } + + async stop(): Promise { + for (const [id, page] of this.pages) { + await page.close(); + this.pages.delete(id); + } + if (this.context) { + await this.context.close(); + this.context = null; + } + logger.info('Trade monitor stopped'); + } +} diff --git a/src/trade/selectors.ts b/src/trade/selectors.ts new file mode 100644 index 0000000..1606430 --- /dev/null +++ b/src/trade/selectors.ts @@ -0,0 +1,30 @@ +// CSS selectors for the POE2 trade website (pathofexile.com/trade2) +// These need to be verified against the live site and updated if the site changes. + +export const SELECTORS = { + // Live search activation button + liveSearchButton: 'button.livesearch-btn, button:has-text("Activate Live Search")', + + // Individual listing rows + listingRow: '.resultset .row, [class*="result"]', + + // Listing by item ID + listingById: (id: string) => `[data-id="${id}"]`, + + // "Travel to Hideout" / "Visit Hideout" button on a listing + travelToHideoutButton: + 'button:has-text("Travel to Hideout"), button:has-text("Visit Hideout"), a:has-text("Travel to Hideout"), [class*="hideout"]', + + // Whisper / copy button on a listing + whisperButton: + '.whisper-btn, button[class*="whisper"], [data-tooltip="Whisper"], button:has-text("Whisper")', + + // "Are you sure?" confirmation dialog + confirmDialog: '[class*="modal"], [class*="dialog"], [class*="confirm"]', + confirmYesButton: + 'button:has-text("Yes"), button:has-text("Confirm"), button:has-text("OK"), button:has-text("Accept")', + confirmNoButton: 'button:has-text("No"), button:has-text("Cancel"), button:has-text("Decline")', + + // Search results container + resultsContainer: '.resultset, [class*="results"]', +} as const; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..82fe694 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,58 @@ +export interface Config { + tradeUrls: string[]; + poe2LogPath: string; + poe2WindowTitle: string; + browserUserDataDir: string; + travelTimeoutMs: number; + stashScanTimeoutMs: number; + waitForMoreItemsMs: number; + betweenTradesDelayMs: number; +} + +export interface Region { + x: number; + y: number; + width: number; + height: number; +} + +export interface ScreenRegions { + stashArea: Region; + priceWarningDialog: Region; + priceWarningNoButton: Region; + inventoryArea: Region; + stashTabArea: Region; +} + +export interface TradeInfo { + searchId: string; + itemIds: string[]; + whisperText: string; + timestamp: number; + tradeUrl: string; + page: unknown; // Playwright Page reference +} + +export interface StashItem { + name: string; + stats: string; + price: string; + position: { x: number; y: number }; +} + +export type TradeState = + | 'IDLE' + | 'TRAVELING' + | 'IN_SELLERS_HIDEOUT' + | 'SCANNING_STASH' + | 'BUYING' + | 'WAITING_FOR_MORE' + | 'GOING_HOME' + | 'IN_HIDEOUT' + | 'FAILED'; + +export interface LogEvent { + timestamp: Date; + type: 'area-entered' | 'whisper-received' | 'trade-accepted' | 'unknown'; + data: Record; +} diff --git a/src/util/clipboard.ts b/src/util/clipboard.ts new file mode 100644 index 0000000..7859549 --- /dev/null +++ b/src/util/clipboard.ts @@ -0,0 +1,13 @@ +import { execSync } from 'child_process'; + +export function readClipboard(): string { + try { + return execSync('powershell -command "Get-Clipboard"', { encoding: 'utf-8' }).trim(); + } catch { + return ''; + } +} + +export function writeClipboard(text: string): void { + execSync(`powershell -command "Set-Clipboard -Value '${text.replace(/'/g, "''")}'"`); +} diff --git a/src/util/logger.ts b/src/util/logger.ts new file mode 100644 index 0000000..ea7a2ee --- /dev/null +++ b/src/util/logger.ts @@ -0,0 +1,12 @@ +import pino from 'pino'; + +export const logger = pino({ + transport: { + target: 'pino-pretty', + options: { + colorize: true, + translateTime: 'HH:MM:ss', + ignore: 'pid,hostname', + }, + }, +}); diff --git a/src/util/retry.ts b/src/util/retry.ts new file mode 100644 index 0000000..94b2b41 --- /dev/null +++ b/src/util/retry.ts @@ -0,0 +1,24 @@ +import { sleep } from './sleep.js'; +import { logger } from './logger.js'; + +export async function retry( + fn: () => Promise, + options: { maxAttempts?: number; delayMs?: number; label?: string } = {}, +): Promise { + const { maxAttempts = 3, delayMs = 1000, label = 'operation' } = options; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return await fn(); + } catch (err) { + if (attempt === maxAttempts) { + logger.error({ err, attempt, label }, `${label} failed after ${maxAttempts} attempts`); + throw err; + } + logger.warn({ err, attempt, label }, `${label} failed, retrying in ${delayMs}ms...`); + await sleep(delayMs * attempt); + } + } + + throw new Error('Unreachable'); +} diff --git a/src/util/sleep.ts b/src/util/sleep.ts new file mode 100644 index 0000000..ad51fa3 --- /dev/null +++ b/src/util/sleep.ts @@ -0,0 +1,8 @@ +export function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export function randomDelay(minMs: number, maxMs: number): Promise { + const delay = minMs + Math.random() * (maxMs - minMs); + return sleep(delay); +} diff --git a/tools/OcrDaemon/OcrDaemon.csproj b/tools/OcrDaemon/OcrDaemon.csproj new file mode 100644 index 0000000..84087a0 --- /dev/null +++ b/tools/OcrDaemon/OcrDaemon.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0-windows10.0.19041.0 + enable + enable + + + + + + + diff --git a/tools/OcrDaemon/Program.cs b/tools/OcrDaemon/Program.cs new file mode 100644 index 0000000..ea38c16 --- /dev/null +++ b/tools/OcrDaemon/Program.cs @@ -0,0 +1,293 @@ +using System.Drawing; +using System.Drawing.Imaging; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Windows.Graphics.Imaging; +using Windows.Media.Ocr; +using Windows.Storage.Streams; + +// Make GDI capture DPI-aware so coordinates match physical pixels +SetProcessDPIAware(); + +// Pre-create the OCR engine (reused across all requests) +var ocrEngine = OcrEngine.TryCreateFromUserProfileLanguages(); +if (ocrEngine == null) +{ + WriteResponse(new ErrorResponse("Failed to create OCR engine. Ensure a language pack is installed.")); + return 1; +} + +// Signal ready +WriteResponse(new ReadyResponse()); + +// JSON options +var jsonOptions = new JsonSerializerOptions +{ + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, +}; + +// Main loop: read one JSON line, handle, write one JSON line +var stdin = Console.In; +string? line; +while ((line = stdin.ReadLine()) != null) +{ + line = line.Trim(); + if (line.Length == 0) continue; + + try + { + var request = JsonSerializer.Deserialize(line, jsonOptions); + if (request == null) + { + WriteResponse(new ErrorResponse("Failed to parse request")); + continue; + } + + switch (request.Cmd?.ToLowerInvariant()) + { + case "ocr": + HandleOcr(request, ocrEngine); + break; + case "screenshot": + HandleScreenshot(request); + break; + case "capture": + HandleCapture(request); + break; + default: + WriteResponse(new ErrorResponse($"Unknown command: {request.Cmd}")); + break; + } + } + catch (Exception ex) + { + WriteResponse(new ErrorResponse(ex.Message)); + } +} + +return 0; + +// ── Handlers ──────────────────────────────────────────────────────────────── + +void HandleOcr(Request req, OcrEngine engine) +{ + using var bitmap = CaptureScreen(req.Region); + var softwareBitmap = BitmapToSoftwareBitmap(bitmap); + var result = engine.RecognizeAsync(softwareBitmap).AsTask().GetAwaiter().GetResult(); + + var lines = new List(); + foreach (var ocrLine in result.Lines) + { + var words = new List(); + foreach (var word in ocrLine.Words) + { + words.Add(new OcrWordResult + { + Text = word.Text, + X = (int)Math.Round(word.BoundingRect.X), + Y = (int)Math.Round(word.BoundingRect.Y), + Width = (int)Math.Round(word.BoundingRect.Width), + Height = (int)Math.Round(word.BoundingRect.Height), + }); + } + lines.Add(new OcrLineResult { Text = ocrLine.Text, Words = words }); + } + + WriteResponse(new OcrResponse { Text = result.Text, Lines = lines }); +} + +void HandleScreenshot(Request req) +{ + if (string.IsNullOrEmpty(req.Path)) + { + WriteResponse(new ErrorResponse("screenshot command requires 'path'")); + return; + } + + using var bitmap = CaptureScreen(req.Region); + var format = GetImageFormat(req.Path); + bitmap.Save(req.Path, format); + WriteResponse(new OkResponse()); +} + +void HandleCapture(Request req) +{ + using var bitmap = CaptureScreen(req.Region); + using var ms = new MemoryStream(); + bitmap.Save(ms, ImageFormat.Png); + var base64 = Convert.ToBase64String(ms.ToArray()); + WriteResponse(new CaptureResponse { Image = base64 }); +} + +// ── Screen Capture ────────────────────────────────────────────────────────── + +Bitmap CaptureScreen(RegionRect? region) +{ + int x, y, w, h; + if (region != null) + { + x = region.X; + y = region.Y; + w = region.Width; + h = region.Height; + } + else + { + // Primary monitor only (0,0 origin, SM_CXSCREEN / SM_CYSCREEN) + x = 0; + y = 0; + w = GetSystemMetrics(0); // SM_CXSCREEN + h = GetSystemMetrics(1); // SM_CYSCREEN + } + + var bitmap = new Bitmap(w, h, PixelFormat.Format32bppArgb); + using var g = Graphics.FromImage(bitmap); + g.CopyFromScreen(x, y, 0, 0, new Size(w, h), CopyPixelOperation.SourceCopy); + return bitmap; +} + +// ── Bitmap → SoftwareBitmap conversion (in-memory) ───────────────────────── + +SoftwareBitmap BitmapToSoftwareBitmap(Bitmap bitmap) +{ + using var ms = new MemoryStream(); + bitmap.Save(ms, ImageFormat.Bmp); + ms.Position = 0; + + var stream = ms.AsRandomAccessStream(); + var decoder = BitmapDecoder.CreateAsync(stream).AsTask().GetAwaiter().GetResult(); + var softwareBitmap = decoder.GetSoftwareBitmapAsync().AsTask().GetAwaiter().GetResult(); + return softwareBitmap; +} + +// ── Response writing ──────────────────────────────────────────────────────── + +void WriteResponse(object response) +{ + var json = JsonSerializer.Serialize(response, jsonOptions); + Console.Out.WriteLine(json); + Console.Out.Flush(); +} + +ImageFormat GetImageFormat(string path) +{ + var ext = Path.GetExtension(path).ToLowerInvariant(); + return ext switch + { + ".jpg" or ".jpeg" => ImageFormat.Jpeg, + ".bmp" => ImageFormat.Bmp, + _ => ImageFormat.Png, + }; +} + +// ── P/Invoke ──────────────────────────────────────────────────────────────── + +[DllImport("user32.dll")] +static extern bool SetProcessDPIAware(); + +[DllImport("user32.dll")] +static extern int GetSystemMetrics(int nIndex); + +// ── Request / Response Models ─────────────────────────────────────────────── + +class Request +{ + [JsonPropertyName("cmd")] + public string? Cmd { get; set; } + + [JsonPropertyName("region")] + public RegionRect? Region { get; set; } + + [JsonPropertyName("path")] + public string? Path { get; set; } +} + +class RegionRect +{ + [JsonPropertyName("x")] + public int X { get; set; } + + [JsonPropertyName("y")] + public int Y { get; set; } + + [JsonPropertyName("width")] + public int Width { get; set; } + + [JsonPropertyName("height")] + public int Height { get; set; } +} + +class ReadyResponse +{ + [JsonPropertyName("ok")] + public bool Ok => true; + + [JsonPropertyName("ready")] + public bool Ready => true; +} + +class OkResponse +{ + [JsonPropertyName("ok")] + public bool Ok => true; +} + +class ErrorResponse(string message) +{ + [JsonPropertyName("ok")] + public bool Ok => false; + + [JsonPropertyName("error")] + public string Error => message; +} + +class OcrResponse +{ + [JsonPropertyName("ok")] + public bool Ok => true; + + [JsonPropertyName("text")] + public string Text { get; set; } = ""; + + [JsonPropertyName("lines")] + public List Lines { get; set; } = []; +} + +class OcrLineResult +{ + [JsonPropertyName("text")] + public string Text { get; set; } = ""; + + [JsonPropertyName("words")] + public List Words { get; set; } = []; +} + +class OcrWordResult +{ + [JsonPropertyName("text")] + public string Text { get; set; } = ""; + + [JsonPropertyName("x")] + public int X { get; set; } + + [JsonPropertyName("y")] + public int Y { get; set; } + + [JsonPropertyName("width")] + public int Width { get; set; } + + [JsonPropertyName("height")] + public int Height { get; set; } +} + +class CaptureResponse +{ + [JsonPropertyName("ok")] + public bool Ok => true; + + [JsonPropertyName("image")] + public string Image { get; set; } = ""; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b1450e5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}