diff --git a/Dockerfile b/Dockerfile index 83bda60..b469bb3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,18 +4,17 @@ FROM oven/bun:latest AS base # Set the working directory WORKDIR /app -# Copy package files +# Copy package files for monorepo COPY package.json bun.lock* ./ # Install dependencies RUN bun install --frozen-lockfile -# Copy source code -COPY src ./src -COPY tsconfig.json ./ +# Copy monorepo packages +COPY packages ./packages # Build the application for production -RUN bun build ./src/index.ts --outdir ./dist --minify --target=bun +RUN cd packages/scraper && bun build ./src/index.ts --target=bun --outdir=../../dist --minify # Multi-stage build - runtime stage FROM oven/bun:latest AS runtime diff --git a/bun.lock b/bun.lock index a355580..bc353ac 100644 --- a/bun.lock +++ b/bun.lock @@ -1,154 +1,83 @@ { "lockfileVersion": 1, + "configVersion": 1, "workspaces": { "": { - "name": "sone4ka-tok", + "name": "marketplace-scrapers-monorepo", + }, + "packages/api-server": { + "name": "@marketplace-scrapers/api-server", + "version": "1.0.0", + "dependencies": { + "@marketplace-scrapers/core": "workspace:*", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + "packages/core": { + "name": "@marketplace-scrapers/core", + "version": "1.0.0", "dependencies": { - "@types/cli-progress": "^3.11.6", "cli-progress": "^3.12.0", "linkedom": "^0.18.12", "unidecode": "^1.1.0", }, "devDependencies": { - "@anthropic-ai/claude-code": "^2.0.1", - "@musistudio/claude-code-router": "^1.0.53", "@types/bun": "latest", + "@types/cli-progress": "^3.11.6", "@types/unidecode": "^1.1.0", }, "peerDependencies": { "typescript": "^5", }, }, + "packages/mcp-server": { + "name": "@marketplace-scrapers/mcp-server", + "version": "1.0.0", + "dependencies": { + "@marketplace-scrapers/core": "workspace:*", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, }, "packages": { - "@anthropic-ai/claude-code": ["@anthropic-ai/claude-code@2.0.1", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.33.5", "@img/sharp-darwin-x64": "^0.33.5", "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" }, "bin": { "claude": "cli.js" } }, "sha512-2SboYcdJ+dsE2K784dbJ4ohVWlAkLZhU7mZG1lebyG6TvGLXLhjc2qTEfCxSeelCjJHhIh/YkNpe06veB4IgBw=="], + "@marketplace-scrapers/api-server": ["@marketplace-scrapers/api-server@workspace:packages/api-server"], - "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.54.0", "", { "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-xyoCtHJnt/qg5GG6IgK+UJEndz8h8ljzt/caKXmq3LfBF81nC/BW6E4x2rOWCZcvsLyVW+e8U5mtIr6UCE/kJw=="], + "@marketplace-scrapers/core": ["@marketplace-scrapers/core@workspace:packages/core"], - "@fastify/accept-negotiator": ["@fastify/accept-negotiator@2.0.1", "", {}, "sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ=="], + "@marketplace-scrapers/mcp-server": ["@marketplace-scrapers/mcp-server@workspace:packages/mcp-server"], - "@fastify/ajv-compiler": ["@fastify/ajv-compiler@4.0.2", "", { "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0" } }, "sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ=="], - - "@fastify/cors": ["@fastify/cors@11.1.0", "", { "dependencies": { "fastify-plugin": "^5.0.0", "toad-cache": "^3.7.0" } }, "sha512-sUw8ed8wP2SouWZTIbA7V2OQtMNpLj2W6qJOYhNdcmINTu6gsxVYXjQiM9mdi8UUDlcoDDJ/W2syPo1WB2QjYA=="], - - "@fastify/error": ["@fastify/error@4.2.0", "", {}, "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ=="], - - "@fastify/fast-json-stringify-compiler": ["@fastify/fast-json-stringify-compiler@5.0.3", "", { "dependencies": { "fast-json-stringify": "^6.0.0" } }, "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ=="], - - "@fastify/forwarded": ["@fastify/forwarded@3.0.1", "", {}, "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw=="], - - "@fastify/merge-json-schemas": ["@fastify/merge-json-schemas@0.2.1", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A=="], - - "@fastify/proxy-addr": ["@fastify/proxy-addr@5.1.0", "", { "dependencies": { "@fastify/forwarded": "^3.0.0", "ipaddr.js": "^2.1.0" } }, "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw=="], - - "@fastify/send": ["@fastify/send@4.1.0", "", { "dependencies": { "@lukeed/ms": "^2.0.2", "escape-html": "~1.0.3", "fast-decode-uri-component": "^1.0.1", "http-errors": "^2.0.0", "mime": "^3" } }, "sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw=="], - - "@fastify/static": ["@fastify/static@8.2.0", "", { "dependencies": { "@fastify/accept-negotiator": "^2.0.0", "@fastify/send": "^4.0.0", "content-disposition": "^0.5.4", "fastify-plugin": "^5.0.0", "fastq": "^1.17.1", "glob": "^11.0.0" } }, "sha512-PejC/DtT7p1yo3p+W7LiUtLMsV8fEvxAK15sozHy9t8kwo5r0uLYmhV/inURmGz1SkHZFz/8CNtHLPyhKcx4SQ=="], - - "@google/genai": ["@google/genai@1.21.0", "", { "dependencies": { "google-auth-library": "^9.14.2", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.11.4" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-k47DECR8BF9z7IJxQd3reKuH2eUnOH5NlJWSe+CKM6nbXx+wH3hmtWQxUQR9M8gzWW1EvFuRVgjQssEIreNZsw=="], - - "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], - - "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], - - "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="], - - "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="], - - "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="], - - "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="], - - "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="], - - "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="], - - "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="], - - "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="], - - "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], - - "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], - - "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], - - "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], - - "@lukeed/ms": ["@lukeed/ms@2.0.2", "", {}, "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA=="], - - "@musistudio/claude-code-router": ["@musistudio/claude-code-router@1.0.53", "", { "dependencies": { "@fastify/static": "^8.2.0", "@musistudio/llms": "^1.0.35", "dotenv": "^16.4.7", "find-process": "^2.0.0", "json5": "^2.2.3", "openurl": "^1.1.1", "rotating-file-stream": "^3.2.7", "shell-quote": "^1.8.3", "tiktoken": "^1.0.21", "uuid": "^11.1.0" }, "bin": { "ccr": "dist/cli.js" } }, "sha512-cNH3dOJu2ECUXHdTbuEyXq7sD12+ie4wqPD85mKz7yg6Xo1HmpFqQQvh4XAhQDBJAWZob6Fuavu+m5f2BwFT/g=="], - - "@musistudio/llms": ["@musistudio/llms@1.0.35", "", { "dependencies": { "@anthropic-ai/sdk": "^0.54.0", "@fastify/cors": "^11.0.1", "@google/genai": "^1.7.0", "dotenv": "^16.5.0", "fastify": "^5.4.0", "google-auth-library": "^10.1.0", "json5": "^2.2.3", "jsonrepair": "^3.13.0", "openai": "^5.6.0", "undici": "^7.10.0", "uuid": "^11.1.0" } }, "sha512-fW7DCHrhzMNtQiaXlAAivSsn+4+vqOYWAURi1OfwESijRDfJk4Gpi0rhedI9o4e0ucr7ftVRO707sOeo/+TJNA=="], - - "@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="], + "@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], "@types/cli-progress": ["@types/cli-progress@3.11.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-cE3+jb9WRlu+uOSAugewNpITJDt1VF8dHOopPO4IABFc3SXYL5WE/+PTz/FCdZRRfIujiWW3n3aMbv1eIGVRWA=="], - "@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="], - - "@types/react": ["@types/react@19.1.9", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA=="], + "@types/node": ["@types/node@25.0.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA=="], "@types/unidecode": ["@types/unidecode@1.1.0", "", {}, "sha512-NTIsFsTe9WRek39/8DDj7KiQ0nU33DHMrKwNHcD1rKlUvn4N0Rc4Di8q/Xavs8bsDZmBa4MMtQA8+HNgwfxC/A=="], - "abstract-logging": ["abstract-logging@2.0.1", "", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="], - - "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - - "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], - - "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], - - "avvio": ["avvio@9.1.0", "", { "dependencies": { "@fastify/error": "^4.0.0", "fastq": "^1.17.1" } }, "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw=="], - - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - - "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], - "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], - "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], - - "bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="], - - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], "cli-progress": ["cli-progress@3.12.0", "", { "dependencies": { "string-width": "^4.2.3" } }, "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A=="], - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], - - "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], - - "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], - - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], "cssom": ["cssom@0.5.0", "", {}, "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw=="], - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], - - "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], - - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - - "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], - "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], @@ -157,260 +86,32 @@ "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], - "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - - "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], - - "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], - "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - - "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], - - "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], - - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - - "fast-json-stringify": ["fast-json-stringify@6.1.1", "", { "dependencies": { "@fastify/merge-json-schemas": "^0.2.0", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0", "json-schema-ref-resolver": "^3.0.0", "rfdc": "^1.2.0" } }, "sha512-DbgptncYEXZqDUOEl4krff4mUiVrTZZVI7BBrQR/T3BqMj/eM1flTC1Uk2uUoLcWCxjT95xKulV/Lc6hhOZsBQ=="], - - "fast-querystring": ["fast-querystring@1.1.2", "", { "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg=="], - - "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], - - "fastify": ["fastify@5.6.1", "", { "dependencies": { "@fastify/ajv-compiler": "^4.0.0", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^6.0.0", "find-my-way": "^9.0.0", "light-my-request": "^6.0.0", "pino": "^9.0.0", "process-warning": "^5.0.0", "rfdc": "^1.3.1", "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } }, "sha512-WjjlOciBF0K8pDUPZoGPhqhKrQJ02I8DKaDIfO51EL0kbSMwQFl85cRwhOvmSDWoukNOdTo27gLN549pLCcH7Q=="], - - "fastify-plugin": ["fastify-plugin@5.1.0", "", {}, "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw=="], - - "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], - - "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], - - "find-my-way": ["find-my-way@9.3.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", "safe-regex2": "^5.0.0" } }, "sha512-eRoFWQw+Yv2tuYlK2pjFS2jGXSxSppAs3hSQjfxVKxM5amECzIgYYc1FEI8ZmhSh/Ig+FrKEz43NLRKJjYCZVg=="], - - "find-process": ["find-process@2.0.0", "", { "dependencies": { "chalk": "~4.1.2", "commander": "^12.1.0", "loglevel": "^1.9.2" }, "bin": { "find-process": "dist/bin/find-process.js" } }, "sha512-YUBQnteWGASJoEVVsOXy6XtKAY2O1FCsWnnvQ8y0YwgY1rZiKeVptnFvMu6RSELZAJOGklqseTnUGGs5D0bKmg=="], - - "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], - - "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], - - "gaxios": ["gaxios@7.1.2", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-/Szrn8nr+2TsQT1Gp8iIe/BEytJmbyfrbFh419DfGQSkEgNEhbPi7JRJuughjkTzPWgU9gBQf5AVu3DbHt0OXA=="], - - "gcp-metadata": ["gcp-metadata@7.0.1", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ=="], - - "glob": ["glob@11.0.3", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.0.3", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA=="], - - "google-auth-library": ["google-auth-library@10.4.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^7.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-CmIrSy1bqMQUsPmA9+hcSbAXL80cFhu40cGMUjCaLpNKVzzvi+0uAHq8GNZxkoGYIsTX4ZQ7e4aInAqWxgn4fg=="], - - "google-logging-utils": ["google-logging-utils@1.1.1", "", {}, "sha512-rcX58I7nqpu4mbKztFeOAObbomBbHU2oIb/d3tJfF3dizGSApqtSwYJigGCooHdnMyQBIw8BrWyK96w3YXgr6A=="], - - "gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="], - - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], "htmlparser2": ["htmlparser2@10.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.1", "entities": "^6.0.0" } }, "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g=="], - "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], - - "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "ipaddr.js": ["ipaddr.js@2.2.0", "", {}, "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA=="], - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], - - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - - "jackspeak": ["jackspeak@4.1.1", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ=="], - - "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], - - "json-schema-ref-resolver": ["json-schema-ref-resolver@3.0.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A=="], - - "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - - "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], - - "jsonrepair": ["jsonrepair@3.13.1", "", { "bin": { "jsonrepair": "bin/cli.js" } }, "sha512-WJeiE0jGfxYmtLwBTEk8+y/mYcaleyLXWaqp5bJu0/ZTSeG0KQq/wWQ8pmnkKenEdN6pdnn6QtcoSUkbqDHWNw=="], - - "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], - - "jws": ["jws@4.0.0", "", { "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg=="], - - "light-my-request": ["light-my-request@6.6.0", "", { "dependencies": { "cookie": "^1.0.1", "process-warning": "^4.0.0", "set-cookie-parser": "^2.6.0" } }, "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A=="], - "linkedom": ["linkedom@0.18.12", "", { "dependencies": { "css-select": "^5.1.0", "cssom": "^0.5.0", "html-escaper": "^3.0.3", "htmlparser2": "^10.0.0", "uhyphen": "^0.2.0" }, "peerDependencies": { "canvas": ">= 2" }, "optionalPeers": ["canvas"] }, "sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q=="], - "loglevel": ["loglevel@1.9.2", "", {}, "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg=="], - - "lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="], - - "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], - - "minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="], - - "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], - - "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], - "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], - "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], - - "openai": ["openai@5.23.2", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-MQBzmTulj+MM5O8SKEk/gL8a7s5mktS9zUtAkU257WjvobGc9nKcBuVwjyEEcb9SI8a8Y2G/mzn3vm9n1Jlleg=="], - - "openurl": ["openurl@1.1.1", "", {}, "sha512-d/gTkTb1i1GKz5k3XE3XFV/PxQ1k45zDqGP2OA7YhgsaLoqm6qRvARAZOFer1fcXritWlGBRCu/UgeS4HAnXAA=="], - - "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], - - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - - "path-scurry": ["path-scurry@2.0.0", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg=="], - - "pino": ["pino@9.12.0", "", { "dependencies": { "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", "slow-redact": "^0.3.0", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-0Gd0OezGvqtqMwgYxpL7P0pSHHzTJ0Lx992h+mNlMtRVfNnqweWmf0JmRWk5gJzHalyd2mxTzKjhiNbGS2Ztfw=="], - - "pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="], - - "pino-std-serializers": ["pino-std-serializers@7.0.0", "", {}, "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA=="], - - "process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], - - "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], - - "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], - - "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - - "ret": ["ret@0.5.0", "", {}, "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw=="], - - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - - "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], - - "rotating-file-stream": ["rotating-file-stream@3.2.7", "", {}, "sha512-SVquhBEVvRFY+nWLUc791Y0MIlyZrEClRZwZFLLRgJKldHyV1z4e2e/dp9LPqCS3AM//uq/c3PnOFgjqnm5P+A=="], - - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "safe-regex2": ["safe-regex2@5.0.0", "", { "dependencies": { "ret": "~0.5.0" } }, "sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw=="], - - "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], - - "secure-json-parse": ["secure-json-parse@4.0.0", "", {}, "sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA=="], - - "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], - - "set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="], - - "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - - "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], - - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - - "slow-redact": ["slow-redact@0.3.0", "", {}, "sha512-cf723wn9JeRIYP9tdtd86GuqoR5937u64Io+CYjlm2i7jvu7g0H+Cp0l0ShAf/4ZL+ISUTVT+8Qzz7RZmp9FjA=="], - - "sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], - - "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], - - "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="], - - "tiktoken": ["tiktoken@1.0.22", "", {}, "sha512-PKvy1rVF1RibfF3JlXBSP0Jrcw2uq3yXdgcEXtKTYn3QJ/cBRBHDnrJ5jHky+MENZ6DIPwNUGWpkVx+7joCpNA=="], - - "toad-cache": ["toad-cache@3.7.0", "", {}, "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw=="], - - "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - - "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], - - "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "uhyphen": ["uhyphen@0.2.0", "", {}, "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA=="], - "undici": ["undici@7.16.0", "", {}, "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g=="], - - "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "unidecode": ["unidecode@1.1.0", "", {}, "sha512-GIp57N6DVVJi8dpeIU6/leJGdv7W65ZSXFLFiNmxvexXkc0nXdqUvhA/qL9KqBKsILxMwg5MnmYNOIDJLb5JVA=="], - "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], - - "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], - - "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - - "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], - - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - - "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], - - "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], - - "@google/genai/google-auth-library": ["google-auth-library@9.15.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^6.1.1", "gcp-metadata": "^6.1.0", "gtoken": "^7.0.0", "jws": "^4.0.0" } }, "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng=="], - - "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], - - "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - - "light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="], - - "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - - "wrap-ansi/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], - - "wrap-ansi/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - - "@google/genai/google-auth-library/gaxios": ["gaxios@6.7.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9", "uuid": "^9.0.1" } }, "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ=="], - - "@google/genai/google-auth-library/gcp-metadata": ["gcp-metadata@6.1.1", "", { "dependencies": { "gaxios": "^6.1.1", "google-logging-utils": "^0.0.2", "json-bigint": "^1.0.0" } }, "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A=="], - - "@google/genai/google-auth-library/gtoken": ["gtoken@7.1.0", "", { "dependencies": { "gaxios": "^6.0.0", "jws": "^4.0.0" } }, "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw=="], - - "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - - "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - - "wrap-ansi/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - - "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - - "@google/genai/google-auth-library/gaxios/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - - "@google/genai/google-auth-library/gaxios/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], - - "@google/genai/google-auth-library/gcp-metadata/google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="], } } diff --git a/docker-compose.yml b/docker-compose.yml index d865103..db297ad 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,4 @@ +# NOT USED, CHECK LOBECHAT CONFIG services: ca-marketplace-scraper: container_name: ca-marketplace-scraper diff --git a/package.json b/package.json index c1b5738..1b086d5 100644 --- a/package.json +++ b/package.json @@ -1,26 +1,7 @@ { - "name": "ca-marketplace-scraper", - "module": "./src/index.ts", - "scripts": { - "start": "bun ./src/index.ts", - "dev": "bun --watch ./src/index.ts", - "build": "bun build ./src/index.ts" - }, - "type": "module", + "name": "marketplace-scrapers-monorepo", + "version": "1.0.0", "private": true, - "devDependencies": { - "@anthropic-ai/claude-code": "^2.0.1", - "@musistudio/claude-code-router": "^1.0.53", - "@types/bun": "latest", - "@types/unidecode": "^1.1.0", - "@types/cli-progress": "^3.11.6" - }, - "peerDependencies": { - "typescript": "^5" - }, - "dependencies": { - "cli-progress": "^3.12.0", - "linkedom": "^0.18.12", - "unidecode": "^1.1.0" - } + "workspaces": ["packages/*"], + "type": "module" } diff --git a/packages/api-server/package.json b/packages/api-server/package.json new file mode 100644 index 0000000..f0e0ba3 --- /dev/null +++ b/packages/api-server/package.json @@ -0,0 +1,21 @@ +{ + "name": "@marketplace-scrapers/api-server", + "version": "1.0.0", + "type": "module", + "module": "./src/index.ts", + "private": true, + "scripts": { + "start": "bun ./src/index.ts", + "dev": "bun --watch ./src/index.ts", + "build": "bun build ./src/index.ts --target=bun --outdir=../../dist/api" + }, + "dependencies": { + "@marketplace-scrapers/core": "workspace:*" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + } +} diff --git a/packages/api-server/src/index.ts b/packages/api-server/src/index.ts new file mode 100644 index 0000000..c35a4e8 --- /dev/null +++ b/packages/api-server/src/index.ts @@ -0,0 +1,30 @@ +import { statusRoute } from "./routes/status"; +import { kijijiRoute } from "./routes/kijiji"; +import { facebookRoute } from "./routes/facebook"; +import { ebayRoute } from "./routes/ebay"; + +const PORT = process.env.PORT || 4005; + +const server = Bun.serve({ + port: PORT as number | string, + idleTimeout: 0, + routes: { + // Health check endpoint + "/api/status": statusRoute, + + // Marketplace search endpoints + "/api/kijiji": kijijiRoute, + "/api/facebook": facebookRoute, + "/api/ebay": ebayRoute, + + // Fallback for unmatched /api routes + "/api/*": Response.json({ message: "Not found" }, { status: 404 }), + }, + + // Fallback for all other routes + fetch(req: Request) { + return new Response("Not Found", { status: 404 }); + }, +}); + +console.log(`API Server running on ${server.hostname}:${server.port}`); diff --git a/packages/api-server/src/routes/ebay.ts b/packages/api-server/src/routes/ebay.ts new file mode 100644 index 0000000..7723308 --- /dev/null +++ b/packages/api-server/src/routes/ebay.ts @@ -0,0 +1,56 @@ +import { fetchEbayItems } from "@marketplace-scrapers/core"; + +/** + * GET /api/ebay?q={query}&minPrice={minPrice}&maxPrice={maxPrice}&strictMode={strictMode}&exclusions={exclusions}&keywords={keywords} + * Search eBay for listings + */ +export async function ebayRoute(req: Request): Promise { + const reqUrl = new URL(req.url); + + const SEARCH_QUERY = + req.headers.get("query") || reqUrl.searchParams.get("q") || null; + if (!SEARCH_QUERY) + return Response.json( + { + message: + "Request didn't have 'query' header or 'q' search parameter!", + }, + { status: 400 }, + ); + + // Parse optional parameters with defaults + const minPrice = reqUrl.searchParams.get("minPrice") + ? parseInt(reqUrl.searchParams.get("minPrice")!) + : undefined; + const maxPrice = reqUrl.searchParams.get("maxPrice") + ? parseInt(reqUrl.searchParams.get("maxPrice")!) + : undefined; + const strictMode = reqUrl.searchParams.get("strictMode") === "true"; + const exclusionsParam = reqUrl.searchParams.get("exclusions"); + const exclusions = exclusionsParam ? exclusionsParam.split(",").map(s => s.trim()) : []; + const keywordsParam = reqUrl.searchParams.get("keywords"); + const keywords = keywordsParam ? keywordsParam.split(",").map(s => s.trim()) : [SEARCH_QUERY]; + + try { + const items = await fetchEbayItems(SEARCH_QUERY, 5, { + minPrice, + maxPrice, + strictMode, + exclusions, + keywords, + }); + if (!items || items.length === 0) + return Response.json( + { message: "Search didn't return any results!" }, + { status: 404 }, + ); + return Response.json(items, { status: 200 }); + } catch (error) { + console.error("eBay scraping error:", error); + const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; + return Response.json( + { message: errorMessage }, + { status: 400 }, + ); + } +} diff --git a/packages/api-server/src/routes/facebook.ts b/packages/api-server/src/routes/facebook.ts new file mode 100644 index 0000000..fe05a52 --- /dev/null +++ b/packages/api-server/src/routes/facebook.ts @@ -0,0 +1,40 @@ +import { fetchFacebookItems } from "@marketplace-scrapers/core"; + +/** + * GET /api/facebook?q={query}&location={location}&cookies={cookies} + * Search Facebook Marketplace for listings + */ +export async function facebookRoute(req: Request): Promise { + const reqUrl = new URL(req.url); + + const SEARCH_QUERY = + req.headers.get("query") || reqUrl.searchParams.get("q") || null; + if (!SEARCH_QUERY) + return Response.json( + { + message: + "Request didn't have 'query' header or 'q' search parameter!", + }, + { status: 400 }, + ); + + const LOCATION = reqUrl.searchParams.get("location") || "toronto"; + const COOKIES_SOURCE = reqUrl.searchParams.get("cookies") || undefined; + + try { + const items = await fetchFacebookItems(SEARCH_QUERY, 5, LOCATION, 25, COOKIES_SOURCE); + if (!items || items.length === 0) + return Response.json( + { message: "Search didn't return any results!" }, + { status: 404 }, + ); + return Response.json(items, { status: 200 }); + } catch (error) { + console.error("Facebook scraping error:", error); + const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; + return Response.json( + { message: errorMessage }, + { status: 400 }, + ); + } +} diff --git a/packages/api-server/src/routes/kijiji.ts b/packages/api-server/src/routes/kijiji.ts new file mode 100644 index 0000000..5adba12 --- /dev/null +++ b/packages/api-server/src/routes/kijiji.ts @@ -0,0 +1,37 @@ +import { fetchKijijiItems } from "@marketplace-scrapers/core"; + +/** + * GET /api/kijiji?q={query} + * Search Kijiji marketplace for listings + */ +export async function kijijiRoute(req: Request): Promise { + const reqUrl = new URL(req.url); + + const SEARCH_QUERY = + req.headers.get("query") || reqUrl.searchParams.get("q") || null; + if (!SEARCH_QUERY) + return Response.json( + { + message: + "Request didn't have 'query' header or 'q' search parameter!", + }, + { status: 400 }, + ); + + try { + const items = await fetchKijijiItems(SEARCH_QUERY, 5); + if (!items) + return Response.json( + { message: "Search didn't return any results!" }, + { status: 404 }, + ); + return Response.json(items, { status: 200 }); + } catch (error) { + console.error("Kijiji scraping error:", error); + const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; + return Response.json( + { message: errorMessage }, + { status: 400 }, + ); + } +} diff --git a/packages/api-server/src/routes/status.ts b/packages/api-server/src/routes/status.ts new file mode 100644 index 0000000..7fb4f7d --- /dev/null +++ b/packages/api-server/src/routes/status.ts @@ -0,0 +1,6 @@ +/** + * Health check endpoint + */ +export function statusRoute(): Response { + return new Response("OK", { status: 200 }); +} diff --git a/packages/api-server/tsconfig.json b/packages/api-server/tsconfig.json new file mode 100644 index 0000000..06ee5b2 --- /dev/null +++ b/packages/api-server/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "lib": ["dom"], + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "paths": { + "@/*": ["./src/*"] + }, + "strict": true, + "noEmit": true + } +} diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..a453cbb --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,21 @@ +{ + "name": "@marketplace-scrapers/core", + "version": "1.0.0", + "type": "module", + "main": "./src/index.ts", + "module": "./src/index.ts", + "private": true, + "dependencies": { + "cli-progress": "^3.12.0", + "linkedom": "^0.18.12", + "unidecode": "^1.1.0" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/unidecode": "^1.1.0", + "@types/cli-progress": "^3.11.6" + }, + "peerDependencies": { + "typescript": "^5" + } +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..2549e8f --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,17 @@ +// Export all scrapers +export { default as fetchKijijiItems, slugify } from "./scrapers/kijiji"; +export type { KijijiListingDetails } from "./scrapers/kijiji"; + +export { default as fetchFacebookItems } from "./scrapers/facebook"; +export type { FacebookListingDetails } from "./scrapers/facebook"; + +export { default as fetchEbayItems } from "./scrapers/ebay"; +export type { EbayListingDetails } from "./scrapers/ebay"; + +// Export shared utilities +export * from "./utils/http"; +export * from "./utils/delay"; +export * from "./utils/format"; + +// Export shared types +export * from "./types/common"; diff --git a/src/ebay.ts b/packages/core/src/scrapers/ebay.ts similarity index 77% rename from src/ebay.ts rename to packages/core/src/scrapers/ebay.ts index 0fc9320..9eb8e9e 100644 --- a/src/ebay.ts +++ b/packages/core/src/scrapers/ebay.ts @@ -1,12 +1,13 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { parseHTML } from "linkedom"; -import cliProgress from "cli-progress"; +import { isRecord } from "../utils/http"; +import { delay } from "../utils/delay"; +import { formatCentsToCurrency } from "../utils/format"; +import type { HTMLString } from "../types/common"; // ----------------------------- Types ----------------------------- -type HTMLString = string; - -type ListingDetails = { +export interface EbayListingDetails { url: string; title: string; description?: string; @@ -21,37 +22,10 @@ type ListingDetails = { endDate?: string; numberOfViews?: number; address?: string | null; -}; +} // ----------------------------- Utilities ----------------------------- -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null; -} - -async function delay(ms: number): Promise { - await new Promise((resolve) => setTimeout(resolve, ms)); -} - -/** - * Turns cents to localized currency string. - */ -function formatCentsToCurrency( - num: number | string | undefined, - locale = "en-US", -): string { - if (num == null) return ""; - const cents = typeof num === "string" ? Number.parseInt(num, 10) : num; - if (Number.isNaN(cents)) return ""; - const dollars = cents / 100; - const formatter = new Intl.NumberFormat(locale, { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - useGrouping: true, - }); - return formatter.format(dollars); -} - /** * Parse eBay currency string like "$1.50 CAD" or "CA $1.50" into cents */ @@ -94,79 +68,6 @@ class HttpError extends Error { } } -// ----------------------------- HTTP Client ----------------------------- - -/** - Fetch HTML with a basic retry strategy and simple rate-limit delay between calls. - - Retries on 429 and 5xx - - Respects X-RateLimit-Reset when present (seconds) -*/ -async function fetchHtml( - url: string, - DELAY_MS: number, - opts?: { - maxRetries?: number; - retryBaseMs?: number; - onRateInfo?: (remaining: string | null, reset: string | null) => void; - }, -): Promise { - const maxRetries = opts?.maxRetries ?? 3; - const retryBaseMs = opts?.retryBaseMs ?? 500; - - for (let attempt = 0; attempt <= maxRetries; attempt++) { - try { - const res = await fetch(url, { - method: "GET", - headers: { - accept: - "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", - "accept-language": "en-CA,en-US;q=0.9,en;q=0.8", - "cache-control": "no-cache", - "upgrade-insecure-requests": "1", - "user-agent": - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36", - }, - }); - - const rateLimitRemaining = res.headers.get("X-RateLimit-Remaining"); - const rateLimitReset = res.headers.get("X-RateLimit-Reset"); - opts?.onRateInfo?.(rateLimitRemaining, rateLimitReset); - - if (!res.ok) { - // Respect 429 reset if provided - if (res.status === 429) { - const resetSeconds = rateLimitReset ? Number(rateLimitReset) : NaN; - const waitMs = Number.isFinite(resetSeconds) - ? Math.max(0, resetSeconds * 1000) - : (attempt + 1) * retryBaseMs; - await delay(waitMs); - continue; - } - // Retry on 5xx - if (res.status >= 500 && res.status < 600 && attempt < maxRetries) { - await delay((attempt + 1) * retryBaseMs); - continue; - } - throw new HttpError( - `Request failed with status ${res.status}`, - res.status, - url, - ); - } - - const html = await res.text(); - // Respect per-request delay to keep at or under REQUESTS_PER_SECOND - await delay(DELAY_MS); - return html; - } catch (err) { - if (attempt >= maxRetries) throw err; - await delay((attempt + 1) * retryBaseMs); - } - } - - throw new Error("Exhausted retries without response"); -} - // ----------------------------- Parsing ----------------------------- /** @@ -177,9 +78,9 @@ function parseEbayListings( keywords: string[], exclusions: string[], strictMode: boolean -): ListingDetails[] { +): EbayListingDetails[] { const { document } = parseHTML(htmlString); - const results: ListingDetails[] = []; + const results: EbayListingDetails[] = []; // Find all listing links by looking for eBay item URLs (/itm/) const linkElements = document.querySelectorAll('a[href*="itm/"]'); @@ -335,7 +236,7 @@ function parseEbayListings( continue; } - const listing: ListingDetails = { + const listing: EbayListingDetails = { url: href, title, listingPrice: { @@ -442,4 +343,4 @@ export default async function fetchEbayItems( } throw err; } -} \ No newline at end of file +} diff --git a/src/facebook.ts b/packages/core/src/scrapers/facebook.ts similarity index 93% rename from src/facebook.ts rename to packages/core/src/scrapers/facebook.ts index 6ba70b9..cc8797f 100644 --- a/src/facebook.ts +++ b/packages/core/src/scrapers/facebook.ts @@ -1,6 +1,10 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { parseHTML } from "linkedom"; import cliProgress from "cli-progress"; +import { isRecord } from "../utils/http"; +import { delay } from "../utils/delay"; +import { formatCentsToCurrency } from "../utils/format"; +import type { HTMLString } from "../types/common"; /** * Facebook Marketplace Scraper @@ -12,8 +16,6 @@ import cliProgress from "cli-progress"; // ----------------------------- Types ----------------------------- -type HTMLString = string; - interface Cookie { name: string; value: string; @@ -63,12 +65,7 @@ interface FacebookMarketplaceSearch { [k: string]: unknown; } -interface FacebookRequireData { - require?: [number, number, number, FacebookMarketplaceSearch, number][]; - [k: string]: unknown; -} - -type ListingDetails = { +export interface FacebookListingDetails { url: string; title: string; description?: string; @@ -92,18 +89,10 @@ type ListingDetails = { }; categoryId?: string; deliveryTypes?: string[]; -}; +} // ----------------------------- Utilities ----------------------------- -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null; -} - -async function delay(ms: number): Promise { - await new Promise((resolve) => setTimeout(resolve, ms)); -} - /** * Load Facebook cookies from file or string */ @@ -364,30 +353,11 @@ function extractFacebookMarketplaceData( return marketplaceData.feed_units.edges.map((edge) => ({ node: edge.node })); } -/** - * Turns cents to localized currency string. - */ -function formatCentsToCurrency( - num: number | string | undefined, - locale = "en-US", -): string { - if (num == null) return ""; - const cents = typeof num === "string" ? Number.parseInt(num, 10) : num; - if (Number.isNaN(cents)) return ""; - const dollars = cents / 100; - const formatter = new Intl.NumberFormat(locale, { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - useGrouping: true, - }); - return formatter.format(dollars); -} - /** Parse Facebook marketplace search results into ListingDetails[] */ -function parseFacebookAds(ads: FacebookAdNode[]): ListingDetails[] { - const results: ListingDetails[] = []; +function parseFacebookAds(ads: FacebookAdNode[]): FacebookListingDetails[] { + const results: FacebookListingDetails[] = []; for (const adJson of ads) { try { @@ -473,11 +443,11 @@ function parseFacebookAds(ads: FacebookAdNode[]): ListingDetails[] { id: listing.marketplace_listing_seller.id } : undefined; - const listingDetails: ListingDetails = { + const listingDetails: FacebookListingDetails = { url, title, listingPrice: { - amountFormatted: priceObj.formatted_amount || formatCentsToCurrency(cents), + amountFormatted: priceObj.formatted_amount || formatCentsToCurrency(cents / 100, "en-CA"), cents, currency: priceObj.currency || "CAD", // Facebook marketplace often uses CAD }, @@ -543,6 +513,7 @@ export default async function fetchFacebookItems( let searchHtml: string; try { searchHtml = await fetchHtml(searchUrl, DELAY_MS, { + maxRetries: 3, onRateInfo: (remaining, reset) => { if (remaining && reset) { console.log( diff --git a/src/kijiji.ts b/packages/core/src/scrapers/kijiji.ts similarity index 65% rename from src/kijiji.ts rename to packages/core/src/scrapers/kijiji.ts index 6cd4091..f171c12 100644 --- a/src/kijiji.ts +++ b/packages/core/src/scrapers/kijiji.ts @@ -2,13 +2,13 @@ import { parseHTML } from "linkedom"; import unidecode from "unidecode"; import cliProgress from "cli-progress"; - -// const unidecode = require("unidecode"); +import { fetchHtml, isRecord, HttpError } from "../utils/http"; +import { delay } from "../utils/delay"; +import { formatCentsToCurrency } from "../utils/format"; +import type { HTMLString } from "../types/common"; // ----------------------------- Types ----------------------------- -type HTMLString = string; - type SearchListing = { name: string; listingLink: string; @@ -36,7 +36,7 @@ interface ApolloListingRoot { [k: string]: unknown; } -type ListingDetails = { +export interface KijijiListingDetails { url: string; title: string; description?: string; @@ -51,14 +51,14 @@ type ListingDetails = { endDate?: string; numberOfViews?: number; address?: string | null; -}; +} // ----------------------------- Utilities ----------------------------- const SEPS = new Set([" ", "–", "—", "/", ":", ";", ",", ".", "-"]); /** - * Slugifies a string for search + * Slugifies a string for Kijiji search URLs */ export function slugify(input: string): string { const s = unidecode(input).toLowerCase(); @@ -84,118 +84,6 @@ export function slugify(input: string): string { return out.join(""); } -/** - * Turns cents to localized currency string. - */ -function formatCentsToCurrency( - num: number | string | undefined, - locale = "en-US", -): string { - if (num == null) return ""; - const cents = typeof num === "string" ? Number.parseInt(num, 10) : num; - if (Number.isNaN(cents)) return ""; - const dollars = cents / 100; - const formatter = new Intl.NumberFormat(locale, { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - useGrouping: true, - }); - return formatter.format(dollars); -} - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null; -} - -async function delay(ms: number): Promise { - await new Promise((resolve) => setTimeout(resolve, ms)); -} - -class HttpError extends Error { - constructor( - message: string, - public readonly status: number, - public readonly url: string, - ) { - super(message); - this.name = "HttpError"; - } -} - -// ----------------------------- HTTP Client ----------------------------- - -/** - Fetch HTML with a basic retry strategy and simple rate-limit delay between calls. - - Retries on 429 and 5xx - - Respects X-RateLimit-Reset when present (seconds) -*/ -async function fetchHtml( - url: string, - DELAY_MS: number, - opts?: { - maxRetries?: number; - retryBaseMs?: number; - onRateInfo?: (remaining: string | null, reset: string | null) => void; - }, -): Promise { - const maxRetries = opts?.maxRetries ?? 3; - const retryBaseMs = opts?.retryBaseMs ?? 500; - - for (let attempt = 0; attempt <= maxRetries; attempt++) { - try { - // console.log(`Fetching: `, url); - const res = await fetch(url, { - method: "GET", - headers: { - accept: - "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", - "accept-language": "en-GB,en-US;q=0.9,en;q=0.8", - "cache-control": "no-cache", - "upgrade-insecure-requests": "1", - "user-agent": - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36", - }, - }); - - const rateLimitRemaining = res.headers.get("X-RateLimit-Remaining"); - const rateLimitReset = res.headers.get("X-RateLimit-Reset"); - opts?.onRateInfo?.(rateLimitRemaining, rateLimitReset); - - if (!res.ok) { - // Respect 429 reset if provided - if (res.status === 429) { - const resetSeconds = rateLimitReset ? Number(rateLimitReset) : NaN; - const waitMs = Number.isFinite(resetSeconds) - ? Math.max(0, resetSeconds * 1000) - : (attempt + 1) * retryBaseMs; - await delay(waitMs); - continue; - } - // Retry on 5xx - if (res.status >= 500 && res.status < 600 && attempt < maxRetries) { - await delay((attempt + 1) * retryBaseMs); - continue; - } - throw new HttpError( - `Request failed with status ${res.status}`, - res.status, - url, - ); - } - - const html = await res.text(); - // Respect per-request delay to keep at or under REQUESTS_PER_SECOND - await delay(DELAY_MS); - return html; - } catch (err) { - if (attempt >= maxRetries) throw err; - await delay((attempt + 1) * retryBaseMs); - } - } - - throw new Error("Exhausted retries without response"); -} - // ----------------------------- Parsing ----------------------------- /** @@ -251,7 +139,7 @@ function parseSearch( function parseListing( htmlString: HTMLString, BASE_URL: string, -): ListingDetails | null { +): KijijiListingDetails | null { const apolloState = extractApolloState(htmlString); if (!apolloState) return null; @@ -278,7 +166,10 @@ function parseListing( } = root as ApolloListingRoot; const cents = price?.amount != null ? Number(price.amount) : undefined; - const amountFormatted = formatCentsToCurrency(cents); + const amountFormatted = + cents != null + ? formatCentsToCurrency(cents / 100, "en-CA") + : undefined; const numberOfViews = metrics?.views != null ? Number(metrics.views) : undefined; @@ -325,6 +216,7 @@ export default async function fetchKijijiItems( console.log(`Fetching search: ${searchUrl}`); const searchHtml = await fetchHtml(searchUrl, DELAY_MS, { + maxRetries: 3, onRateInfo: (remaining, reset) => { if (remaining && reset) { console.log( @@ -358,10 +250,11 @@ export default async function fetchKijijiItems( let currentProgress = 0; progressBar.start(totalProgress, currentProgress); - const items: ListingDetails[] = []; + const items: KijijiListingDetails[] = []; for (const link of listingLinks) { try { const html = await fetchHtml(link, DELAY_MS, { + maxRetries: 3, onRateInfo: (remaining, reset) => { if (remaining && reset) { console.log( @@ -378,7 +271,7 @@ export default async function fetchKijijiItems( } catch (err) { if (err instanceof HttpError) { console.error( - "\n" + `Failed to fetch ${link}\n - ${err.status} ${err.message}`, + "\n" + `Failed to fetch ${link}\n - ${err.statusCode} ${err.message}`, ); } else { console.error( diff --git a/packages/core/src/types/common.ts b/packages/core/src/types/common.ts new file mode 100644 index 0000000..a39854c --- /dev/null +++ b/packages/core/src/types/common.ts @@ -0,0 +1,20 @@ +/** HTML string alias for better type clarity */ +export type HTMLString = string; + +/** Currency price object with formatting options */ +export interface Price { + amountFormatted: string; + cents: number; + currency: string; +} + +/** Base listing details common across all marketplaces */ +export interface ListingDetails { + url: string; + title: string; + listingPrice: Price; + listingType: string; + listingStatus: string; + address?: string | null; + creationDate?: string; +} diff --git a/packages/core/src/utils/delay.ts b/packages/core/src/utils/delay.ts new file mode 100644 index 0000000..a48063e --- /dev/null +++ b/packages/core/src/utils/delay.ts @@ -0,0 +1,8 @@ +/** + * Delay execution for a specified number of milliseconds + * @param ms - Milliseconds to delay + * @returns A promise that resolves after the specified delay + */ +export function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/packages/core/src/utils/format.ts b/packages/core/src/utils/format.ts new file mode 100644 index 0000000..ca50fb1 --- /dev/null +++ b/packages/core/src/utils/format.ts @@ -0,0 +1,21 @@ +/** + * Format cents to a human-readable currency string + * @param cents - Amount in cents (integer) + * @param locale - Locale string for formatting (e.g., 'en-CA', 'en-US') + * @returns Formatted currency string + */ +export function formatCentsToCurrency(cents: number, locale: string = "en-CA"): string { + try { + const formatter = new Intl.NumberFormat(locale, { + style: "currency", + currency: "CAD", + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }); + return formatter.format(cents / 100); + } catch (error) { + // Fallback if locale is not supported + const dollars = (cents / 100).toFixed(2); + return `$${dollars}`; + } +} diff --git a/packages/core/src/utils/http.ts b/packages/core/src/utils/http.ts new file mode 100644 index 0000000..dc2a223 --- /dev/null +++ b/packages/core/src/utils/http.ts @@ -0,0 +1,87 @@ +/** Custom error class for HTTP-related failures */ +export class HttpError extends Error { + constructor( + public statusCode: number, + message: string + ) { + super(message); + this.name = "HttpError"; + } +} + +/** Type guard to check if a value is a record (object) */ +export function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +/** + * Fetch HTML content from a URL with automatic retries + * @param url - The URL to fetch + * @param delayMs - Delay in milliseconds between retries + * @param opts - Optional fetch options + * @returns The HTML content as a string + * @throws HttpError if all retries are exhausted + */ +export async function fetchHtml( + url: string, + delayMs: number, + opts?: RequestInit +): Promise { + const maxAttempts = 3; + let lastError: Error | null = null; + + for (let attempt = 0; attempt < maxAttempts; attempt++) { + try { + const response = await fetch(url, opts); + + // Check for rate limiting + if (response.status === 429) { + const retryAfter = response.headers.get("Retry-After"); + const waitTime = retryAfter ? parseInt(retryAfter) * 1000 : delayMs * (attempt + 1); + console.warn( + `Rate limited. Retrying after ${waitTime}ms...` + ); + await new Promise((resolve) => setTimeout(resolve, waitTime)); + continue; + } + + // Check for server errors + if (response.status >= 500) { + lastError = new HttpError( + response.status, + `Server error: ${response.status}` + ); + if (attempt < maxAttempts - 1) { + await new Promise((resolve) => + setTimeout(resolve, delayMs * (attempt + 1)) + ); + continue; + } + throw lastError; + } + + // Check for successful response + if (!response.ok) { + throw new HttpError( + response.status, + `HTTP ${response.status}: ${response.statusText}` + ); + } + + return await response.text(); + } catch (error) { + lastError = + error instanceof Error + ? error + : new Error("Unknown error during fetch"); + + if (attempt < maxAttempts - 1) { + await new Promise((resolve) => + setTimeout(resolve, delayMs * (attempt + 1)) + ); + } + } + } + + throw lastError || new HttpError(0, "Failed to fetch after retries"); +} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..06ee5b2 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "lib": ["dom"], + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "paths": { + "@/*": ["./src/*"] + }, + "strict": true, + "noEmit": true + } +} diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json new file mode 100644 index 0000000..06841c3 --- /dev/null +++ b/packages/mcp-server/package.json @@ -0,0 +1,21 @@ +{ + "name": "@marketplace-scrapers/mcp-server", + "version": "1.0.0", + "type": "module", + "module": "./src/index.ts", + "private": true, + "scripts": { + "start": "bun ./src/index.ts", + "dev": "bun --watch ./src/index.ts", + "build": "bun build ./src/index.ts --target=bun --outdir=../../dist/mcp" + }, + "dependencies": { + "@marketplace-scrapers/core": "workspace:*" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + } +} diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts new file mode 100644 index 0000000..9c8ff3a --- /dev/null +++ b/packages/mcp-server/src/index.ts @@ -0,0 +1,33 @@ +import { handleMcpRequest } from "./protocol/handler"; +import { serverCard } from "./protocol/metadata"; + +const PORT = process.env.MCP_PORT || 4006; + +const server = Bun.serve({ + port: PORT as number | string, + idleTimeout: 0, + routes: { + // MCP metadata discovery endpoint + "/.well-known/mcp/server-card.json": new Response(JSON.stringify(serverCard), { + headers: { "Content-Type": "application/json" }, + }), + + // MCP JSON-RPC 2.0 protocol endpoint + "/mcp": async (req: Request) => { + if (req.method === "POST") { + return await handleMcpRequest(req); + } + return Response.json( + { message: "MCP endpoint requires POST request" }, + { status: 405 } + ); + }, + }, + + // Fallback for all other routes + fetch(req: Request) { + return new Response("Not Found", { status: 404 }); + }, +}); + +console.log(`MCP Server running on ${server.hostname}:${server.port}`); diff --git a/packages/mcp-server/src/protocol/handler.ts b/packages/mcp-server/src/protocol/handler.ts new file mode 100644 index 0000000..910f818 --- /dev/null +++ b/packages/mcp-server/src/protocol/handler.ts @@ -0,0 +1,185 @@ +import { fetchKijijiItems, fetchFacebookItems, fetchEbayItems } from "@marketplace-scrapers/core"; +import { tools } from "./tools"; + +/** + * Handle MCP JSON-RPC 2.0 protocol requests + */ +export async function handleMcpRequest(req: Request): Promise { + try { + const body = await req.json(); + + // Validate JSON-RPC 2.0 format + if (!body.jsonrpc || body.jsonrpc !== "2.0" || !body.method) { + return Response.json( + { + jsonrpc: "2.0", + error: { code: -32600, message: "Invalid Request" }, + id: body.id, + }, + { status: 400 } + ); + } + + const { method, params, id } = body; + + // Handle initialize method + if (method === "initialize") { + return Response.json({ + jsonrpc: "2.0", + id, + result: { + protocolVersion: "2025-06-18", + capabilities: { + tools: { + listChanged: true, + }, + }, + serverInfo: { + name: "marketplace-scrapers", + version: "1.0.0", + }, + instructions: "Use search_kijiji, search_facebook, or search_ebay tools to find listings across Canadian marketplaces", + }, + }); + } + + // Handle tools/list method + if (method === "tools/list") { + return Response.json({ + jsonrpc: "2.0", + id, + result: { + tools, + }, + }); + } + + // Handle notifications (messages without id field should not get a response) + if (!id) { + // Notifications don't require a response + if (method === "notifications/initialized") { + // Client initialized successfully, no response needed + return new Response(null, { status: 204 }); + } + if (method === "notifications/progress") { + // Progress notifications, no response needed + return new Response(null, { status: 204 }); + } + // Unknown notification - still no response for notifications + return new Response(null, { status: 204 }); + } + + // Handle tools/call method + if (method === "tools/call") { + const { name, arguments: args } = params || {}; + + if (!name || !args) { + return Response.json( + { + jsonrpc: "2.0", + id, + error: { code: -32602, message: "Invalid params: name and arguments required" }, + }, + { status: 400 } + ); + } + + // Route tool calls to appropriate handlers + try { + let result; + + if (name === "search_kijiji") { + const query = args.query; + if (!query) { + return Response.json({ + jsonrpc: "2.0", + id, + error: { code: -32602, message: "query parameter is required" }, + }); + } + const items = await fetchKijijiItems(query, args.maxItems || 5); + result = items || []; + } else if (name === "search_facebook") { + const query = args.query; + if (!query) { + return Response.json({ + jsonrpc: "2.0", + id, + error: { code: -32602, message: "query parameter is required" }, + }); + } + const items = await fetchFacebookItems( + query, + args.maxItems || 5, + args.location || "toronto", + 25, + args.cookiesSource + ); + result = items || []; + } else if (name === "search_ebay") { + const query = args.query; + if (!query) { + return Response.json({ + jsonrpc: "2.0", + id, + error: { code: -32602, message: "query parameter is required" }, + }); + } + const items = await fetchEbayItems(query, args.maxItems || 5, { + minPrice: args.minPrice, + maxPrice: args.maxPrice, + strictMode: args.strictMode || false, + exclusions: args.exclusions || [], + keywords: args.keywords || [query], + }); + result = items || []; + } else { + return Response.json({ + jsonrpc: "2.0", + id, + error: { code: -32601, message: `Unknown tool: ${name}` }, + }); + } + + return Response.json({ + jsonrpc: "2.0", + id, + result: { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + }, + }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "Unknown error"; + return Response.json({ + jsonrpc: "2.0", + id, + error: { code: -32603, message: `Tool execution failed: ${errorMessage}` }, + }); + } + } + + // Method not found + return Response.json( + { + jsonrpc: "2.0", + id, + error: { code: -32601, message: `Method not found: ${method}` }, + }, + { status: 404 } + ); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "Unknown error"; + return Response.json( + { + jsonrpc: "2.0", + error: { code: -32700, message: `Parse error: ${errorMessage}` }, + }, + { status: 400 } + ); + } +} diff --git a/packages/mcp-server/src/protocol/metadata.ts b/packages/mcp-server/src/protocol/metadata.ts new file mode 100644 index 0000000..24db8d2 --- /dev/null +++ b/packages/mcp-server/src/protocol/metadata.ts @@ -0,0 +1,25 @@ +/** + * MCP Server metadata for discovery + */ + +export const serverCard = { + $schema: "https://static.modelcontextprotocol.io/schemas/mcp-server-card/v1.json", + version: "1.0", + protocolVersion: "2025-06-18", + serverInfo: { + name: "marketplace-scrapers", + title: "Marketplace Scrapers MCP Server", + version: "1.0.0", + }, + transport: { + type: "streamable-http", + endpoint: "/mcp", + }, + capabilities: { + tools: { + listChanged: true, + }, + }, + description: "Scrapes marketplace listings from Kijiji, Facebook Marketplace, and eBay", + tools: "dynamic", +}; diff --git a/packages/mcp-server/src/protocol/tools.ts b/packages/mcp-server/src/protocol/tools.ts new file mode 100644 index 0000000..b0fc96e --- /dev/null +++ b/packages/mcp-server/src/protocol/tools.ts @@ -0,0 +1,95 @@ +/** + * MCP tool definitions for marketplace scrapers + */ + +export const tools = [ + { + name: "search_kijiji", + description: "Search Kijiji marketplace for listings matching a query", + inputSchema: { + type: "object", + properties: { + query: { + type: "string", + description: "Search query for Kijiji listings", + }, + maxItems: { + type: "number", + description: "Maximum number of items to return", + default: 5, + }, + }, + required: ["query"], + }, + }, + { + name: "search_facebook", + description: "Search Facebook Marketplace for listings matching a query", + inputSchema: { + type: "object", + properties: { + query: { + type: "string", + description: "Search query for Facebook Marketplace listings", + }, + location: { + type: "string", + description: "Location for search (e.g., 'toronto')", + default: "toronto", + }, + maxItems: { + type: "number", + description: "Maximum number of items to return", + default: 5, + }, + cookiesSource: { + type: "string", + description: "Optional Facebook session cookies source", + }, + }, + required: ["query"], + }, + }, + { + name: "search_ebay", + description: "Search eBay for listings matching a query", + inputSchema: { + type: "object", + properties: { + query: { + type: "string", + description: "Search query for eBay listings", + }, + minPrice: { + type: "number", + description: "Minimum price filter", + }, + maxPrice: { + type: "number", + description: "Maximum price filter", + }, + strictMode: { + type: "boolean", + description: "Enable strict search mode", + default: false, + }, + exclusions: { + type: "array", + items: { type: "string" }, + description: "Terms to exclude from results", + }, + keywords: { + type: "array", + items: { type: "string" }, + description: "Keywords to include in search", + }, + maxItems: { + type: "number", + description: "Maximum number of items to return", + default: 5, + }, + }, + required: ["query"], + }, + }, +]; diff --git a/packages/mcp-server/tsconfig.json b/packages/mcp-server/tsconfig.json new file mode 100644 index 0000000..06ee5b2 --- /dev/null +++ b/packages/mcp-server/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "lib": ["dom"], + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "paths": { + "@/*": ["./src/*"] + }, + "strict": true, + "noEmit": true + } +} diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 113ef62..0000000 --- a/src/index.ts +++ /dev/null @@ -1,142 +0,0 @@ -import fetchKijijiItems from "@/kijiji"; -import fetchFacebookItems from "@/facebook"; -import fetchEbayItems from "@/ebay"; - -const PORT = process.env.PORT || 4005; - -const server = Bun.serve({ - port: PORT, - idleTimeout: 0, - routes: { - // Static routes - "/api/status": new Response("OK"), - - // Dynamic routes - "/api/kijiji": async (req: Request) => { - const reqUrl = new URL(req.url); - - const SEARCH_QUERY = - req.headers.get("query") || reqUrl.searchParams.get("q") || null; - if (!SEARCH_QUERY) - return Response.json( - { - message: - "Request didn't have 'query' header or 'q' search parameter!", - }, - { status: 400 }, - ); - - const items = await fetchKijijiItems(SEARCH_QUERY, 5); - if (!items) - return Response.json( - { message: "Search didn't return any results!" }, - { status: 404 }, - ); - return Response.json(items, { status: 200 }); - }, - - "/api/facebook": async (req: Request) => { - const reqUrl = new URL(req.url); - - const SEARCH_QUERY = - req.headers.get("query") || reqUrl.searchParams.get("q") || null; - if (!SEARCH_QUERY) - return Response.json( - { - message: - "Request didn't have 'query' header or 'q' search parameter!", - }, - { status: 400 }, - ); - - const LOCATION = reqUrl.searchParams.get("location") || "toronto"; - const COOKIES_SOURCE = reqUrl.searchParams.get("cookies") || undefined; - - try { - const items = await fetchFacebookItems(SEARCH_QUERY, 5, LOCATION, 25, COOKIES_SOURCE); - if (!items || items.length === 0) - return Response.json( - { message: "Search didn't return any results!" }, - { status: 404 }, - ); - return Response.json(items, { status: 200 }); - } catch (error) { - console.error("Facebook scraping error:", error); - const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; - return Response.json( - { message: errorMessage }, - { status: 400 }, - ); - } - }, - - "/api/ebay": async (req: Request) => { - const reqUrl = new URL(req.url); - - const SEARCH_QUERY = - req.headers.get("query") || reqUrl.searchParams.get("q") || null; - if (!SEARCH_QUERY) - return Response.json( - { - message: - "Request didn't have 'query' header or 'q' search parameter!", - }, - { status: 400 }, - ); - - // Parse optional parameters with defaults - const minPrice = reqUrl.searchParams.get("minPrice") - ? parseInt(reqUrl.searchParams.get("minPrice")!) - : undefined; - const maxPrice = reqUrl.searchParams.get("maxPrice") - ? parseInt(reqUrl.searchParams.get("maxPrice")!) - : undefined; - const strictMode = reqUrl.searchParams.get("strictMode") === "true"; - const exclusionsParam = reqUrl.searchParams.get("exclusions"); - const exclusions = exclusionsParam ? exclusionsParam.split(",").map(s => s.trim()) : []; - const keywordsParam = reqUrl.searchParams.get("keywords"); - const keywords = keywordsParam ? keywordsParam.split(",").map(s => s.trim()) : [SEARCH_QUERY]; - - try { - const items = await fetchEbayItems(SEARCH_QUERY, 5, { - minPrice, - maxPrice, - strictMode, - exclusions, - keywords, - }); - if (!items || items.length === 0) - return Response.json( - { message: "Search didn't return any results!" }, - { status: 404 }, - ); - return Response.json(items, { status: 200 }); - } catch (error) { - console.error("eBay scraping error:", error); - const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; - return Response.json( - { message: errorMessage }, - { status: 400 }, - ); - } - }, - - // Wildcard route for all routes that start with "/api/" and aren't otherwise matched - "/api/*": Response.json({ message: "Not found" }, { status: 404 }), - - // // Serve a file by buffering it in memory - // "/favicon.ico": new Response(await Bun.file("./favicon.ico").bytes(), { - // headers: { - // "Content-Type": "image/x-icon", - // }, - // }), - }, - - // (optional) fallback for unmatched routes: - // Required if Bun's version < 1.2.3 - fetch(req: Request) { - return new Response("Not Found", { status: 404 }); - }, -}); - -console.log(`Serving on ${server.hostname}:${server.port}`); diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 2aeb3d1..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "compilerOptions": { - // Environment setup & latest features - "lib": ["dom"], - "target": "ESNext", - "module": "ESNext", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, - - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "noImplicitAny": true, - - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false, - - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] -}