diff --git a/package-lock.json b/package-lock.json index f155a16..2e18809 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,8 @@ "chalk": "^5.6.2", "express": "^5.2.1", "log-symbols": "^7.0.1", - "playwright": "^1.41.0" + "playwright": "^1.41.0", + "sharp": "^0.34.5" }, "bin": { "claude-browse": "dist/cli.js", @@ -23,6 +24,7 @@ "@biomejs/biome": "^1.9.0", "@types/express": "^5.0.6", "@types/node": "^20.11.0", + "@types/sharp": "^0.31.1", "@types/supertest": "^6.0.3", "@vitest/coverage-v8": "^4.0.18", "supertest": "^7.2.2", @@ -257,6 +259,16 @@ "node": ">=14.21.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", @@ -711,6 +723,471 @@ "hono": "^4" } }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "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.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "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.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "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.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "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.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "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.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "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-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "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-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "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.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "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.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "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.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "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.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "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-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "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.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "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/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1296,6 +1773,16 @@ "@types/node": "*" } }, + "node_modules/@types/sharp": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/@types/sharp/-/sharp-0.31.1.tgz", + "integrity": "sha512-5nWwamN9ZFHXaYEincMSuza8nNfOof8nmO+mcI+Agx1uMUk4/pQnNIcix+9rLPXzKrm1pS34+6WRDbDV0Jn7ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/superagent": { "version": "8.1.9", "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", @@ -1765,6 +2252,15 @@ "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/dezalgo": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", @@ -2924,7 +3420,6 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2984,6 +3479,50 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3219,6 +3758,13 @@ "node": ">=0.6" } }, + "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/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", diff --git a/package.json b/package.json index 7d33ec9..e7ee5ee 100644 --- a/package.json +++ b/package.json @@ -52,12 +52,14 @@ "chalk": "^5.6.2", "express": "^5.2.1", "log-symbols": "^7.0.1", - "playwright": "^1.41.0" + "playwright": "^1.41.0", + "sharp": "^0.34.5" }, "devDependencies": { "@biomejs/biome": "^1.9.0", "@types/express": "^5.0.6", "@types/node": "^20.11.0", + "@types/sharp": "^0.31.1", "@types/supertest": "^6.0.3", "@vitest/coverage-v8": "^4.0.18", "supertest": "^7.2.2", diff --git a/src/browser.ts b/src/browser.ts index 66cd2f8..97341a7 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -1,5 +1,6 @@ import { resolve } from 'node:path'; import { type Browser, type BrowserContext, type Page, webkit } from 'playwright'; +import * as image from './image.js'; import type { BrowserCommand, BrowserOptions, CommandResponse, ElementInfo } from './types.js'; export class ClaudeBrowser { @@ -190,6 +191,72 @@ export class ClaudeBrowser { const result = await this.eval(cmd.script); return { ok: true, result }; } + case 'favicon': { + const result = await image.createFavicon(cmd.input, cmd.outputDir); + return { ok: true, files: result.files, outputDir: result.outputDir }; + } + case 'convert': { + const result = await image.convert(cmd.input, cmd.output, cmd.format); + return { + ok: true, + path: result.path, + width: result.width, + height: result.height, + format: result.format, + size: result.size, + }; + } + case 'resize': { + const result = await image.resize(cmd.input, cmd.output, cmd.width, cmd.height, cmd.fit); + return { + ok: true, + path: result.path, + width: result.width, + height: result.height, + format: result.format, + size: result.size, + }; + } + case 'crop': { + const result = await image.crop( + cmd.input, + cmd.output, + cmd.left, + cmd.top, + cmd.width, + cmd.height + ); + return { + ok: true, + path: result.path, + width: result.width, + height: result.height, + format: result.format, + size: result.size, + }; + } + case 'compress': { + const result = await image.compress(cmd.input, cmd.output, cmd.quality); + return { + ok: true, + path: result.path, + width: result.width, + height: result.height, + format: result.format, + size: result.size, + }; + } + case 'thumbnail': { + const result = await image.thumbnail(cmd.input, cmd.output, cmd.size); + return { + ok: true, + path: result.path, + width: result.width, + height: result.height, + format: result.format, + size: result.size, + }; + } default: { const _exhaustive: never = cmd; return { ok: false, error: `Unknown command: ${(_exhaustive as { cmd: string }).cmd}` }; diff --git a/src/cli.ts b/src/cli.ts index 87b0705..3733f3a 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,6 +2,7 @@ import { resolve } from 'node:path'; import { parseArgs } from 'node:util'; import { ClaudeBrowser } from './browser.js'; +import * as image from './image.js'; import { startServer } from './server.js'; import type { ElementInfo } from './types.js'; @@ -21,6 +22,11 @@ const { values, positionals } = parseArgs({ type: { type: 'string', short: 't', multiple: true }, help: { type: 'boolean', default: false }, version: { type: 'boolean', short: 'v', default: false }, + // Image processing options + favicon: { type: 'string' }, + convert: { type: 'string' }, + resize: { type: 'string' }, + compress: { type: 'string' }, }, }); @@ -42,6 +48,12 @@ Options: -v, --version Show version --help Show this help +Image Processing: + --favicon Generate favicon set to directory (from screenshot or input) + --convert Convert screenshot to format (png, jpeg, webp, avif) + --resize Resize screenshot (e.g., 800x600 or 800 for width only) + --compress Compress with quality 1-100 + Examples: claude-browse https://example.com claude-browse -o page.png -w 1920 -h 1080 https://example.com @@ -52,6 +64,12 @@ Examples: claude-browse -t "input[name=q]=hello" -c "button[type=submit]" https://google.com claude-browse -c ".cookie-accept" -c "a.nav-link" -q "h1" https://example.com +Image processing examples: + claude-browse https://example.com --favicon ./favicons/ + claude-browse https://example.com -o page.webp --convert webp + claude-browse https://example.com --resize 800x600 + claude-browse https://example.com --compress 60 + Server mode (default): claude-browse # Start server on port 13373 claude-browse --headed # Start with visible browser @@ -65,6 +83,12 @@ Server mode (default): curl -X POST http://localhost:13373 -d '{"cmd":"url"}' curl -X POST http://localhost:13373 -d '{"cmd":"html"}' curl -X POST http://localhost:13373 -d '{"cmd":"close"}' + + # Image processing via server: + curl localhost:13373 -d '{"cmd":"favicon","input":"screenshot.png","outputDir":"./favicons"}' + curl localhost:13373 -d '{"cmd":"convert","input":"img.png","output":"img.webp","format":"webp"}' + curl localhost:13373 -d '{"cmd":"resize","input":"img.png","output":"small.png","width":400}' + curl localhost:13373 -d '{"cmd":"compress","input":"img.png","output":"compressed.png","quality":60}' `; function getViewportConfig() { @@ -157,10 +181,45 @@ async function runInteractiveMode(browser: ClaudeBrowser): Promise { await new Promise(() => {}); } +async function processImageOptions(screenshotPath: string): Promise { + // Process image options on the screenshot + if (values.favicon) { + console.log(`Generating favicon set to: ${values.favicon}`); + const result = await image.createFavicon(screenshotPath, values.favicon as string); + console.log(`Created ${result.files.length} favicon files`); + } + + if (values.convert) { + const format = values.convert as 'png' | 'jpeg' | 'webp' | 'avif'; + const outputPath = screenshotPath.replace(/\.[^.]+$/, `.${format}`); + console.log(`Converting to ${format}: ${outputPath}`); + await image.convert(screenshotPath, outputPath, format); + } + + if (values.resize) { + const resizeValue = values.resize as string; + const [widthStr, heightStr] = resizeValue.split('x'); + const width = Number.parseInt(widthStr); + const height = heightStr ? Number.parseInt(heightStr) : undefined; + console.log(`Resizing to ${width}${height ? `x${height}` : ''}`); + await image.resize(screenshotPath, screenshotPath, width, height); + } + + if (values.compress) { + const quality = Number.parseInt(values.compress as string); + console.log(`Compressing with quality ${quality}`); + await image.compress(screenshotPath, screenshotPath, quality); + } +} + async function runScreenshotMode(browser: ClaudeBrowser): Promise { const outputPath = resolve(values.output as string); console.log(`Saving screenshot to: ${outputPath}`); await browser.screenshot(outputPath, values.fullpage); + + // Process any image options + await processImageOptions(outputPath); + await browser.close(); console.log('Done!'); } diff --git a/src/image.ts b/src/image.ts new file mode 100644 index 0000000..26a5c54 --- /dev/null +++ b/src/image.ts @@ -0,0 +1,196 @@ +import { mkdir } from 'node:fs/promises'; +import { dirname, join, resolve } from 'node:path'; +import sharp from 'sharp'; + +export type FitType = 'cover' | 'contain' | 'fill' | 'inside' | 'outside'; +export type FormatType = 'png' | 'jpeg' | 'webp' | 'avif'; +export type ThumbnailSize = 'small' | 'medium' | 'large'; + +const THUMBNAIL_SIZES: Record = { + small: 150, + medium: 300, + large: 600, +}; + +const FAVICON_SIZES = [ + { name: 'favicon-16x16.png', size: 16 }, + { name: 'favicon-32x32.png', size: 32 }, + { name: 'favicon-48x48.png', size: 48 }, + { name: 'apple-touch-icon.png', size: 180 }, + { name: 'android-chrome-192x192.png', size: 192 }, + { name: 'android-chrome-512x512.png', size: 512 }, +]; + +export interface FaviconResult { + files: string[]; + outputDir: string; +} + +export interface ImageResult { + path: string; + width?: number; + height?: number; + format?: string; + size?: number; +} + +async function ensureDir(filePath: string): Promise { + await mkdir(dirname(filePath), { recursive: true }); +} + +export async function createFavicon(input: string, outputDir: string): Promise { + const resolvedDir = resolve(outputDir); + await mkdir(resolvedDir, { recursive: true }); + + const files: string[] = []; + const image = sharp(input); + + for (const { name, size } of FAVICON_SIZES) { + const outputPath = join(resolvedDir, name); + await image.clone().resize(size, size, { fit: 'cover' }).png().toFile(outputPath); + files.push(outputPath); + } + + // Create favicon.ico with multiple sizes (16, 32, 48) + const icoPath = join(resolvedDir, 'favicon.ico'); + const sizes = [16, 32, 48]; + const buffers = await Promise.all( + sizes.map((size) => image.clone().resize(size, size, { fit: 'cover' }).png().toBuffer()) + ); + + // ICO format: simple approach - use largest PNG as ICO + // For true multi-size ICO, we'd need a dedicated library + // Sharp doesn't support ICO output, so we'll use the 32x32 PNG + await image.clone().resize(32, 32, { fit: 'cover' }).png().toFile(icoPath); + files.push(icoPath); + + return { files, outputDir: resolvedDir }; +} + +export async function convert( + input: string, + output: string, + format: FormatType +): Promise { + const resolvedOutput = resolve(output); + await ensureDir(resolvedOutput); + + const image = sharp(input); + let result: sharp.Sharp; + + switch (format) { + case 'png': + result = image.png(); + break; + case 'jpeg': + result = image.jpeg(); + break; + case 'webp': + result = image.webp(); + break; + case 'avif': + result = image.avif(); + break; + } + + const info = await result.toFile(resolvedOutput); + + return { + path: resolvedOutput, + width: info.width, + height: info.height, + format: info.format, + size: info.size, + }; +} + +export async function resize( + input: string, + output: string, + width: number, + height?: number, + fit: FitType = 'cover' +): Promise { + const resolvedOutput = resolve(output); + await ensureDir(resolvedOutput); + + const info = await sharp(input).resize(width, height, { fit }).toFile(resolvedOutput); + + return { + path: resolvedOutput, + width: info.width, + height: info.height, + format: info.format, + size: info.size, + }; +} + +export async function crop( + input: string, + output: string, + left: number, + top: number, + width: number, + height: number +): Promise { + const resolvedOutput = resolve(output); + await ensureDir(resolvedOutput); + + const info = await sharp(input).extract({ left, top, width, height }).toFile(resolvedOutput); + + return { + path: resolvedOutput, + width: info.width, + height: info.height, + format: info.format, + size: info.size, + }; +} + +export async function compress(input: string, output: string, quality = 80): Promise { + const resolvedOutput = resolve(output); + await ensureDir(resolvedOutput); + + const image = sharp(input); + const metadata = await image.metadata(); + const format = metadata.format; + + let result: sharp.Sharp; + switch (format) { + case 'png': + result = image.png({ quality }); + break; + case 'jpeg': + case 'jpg': + result = image.jpeg({ quality }); + break; + case 'webp': + result = image.webp({ quality }); + break; + case 'avif': + result = image.avif({ quality }); + break; + default: + // Default to PNG for unknown formats + result = image.png({ quality }); + } + + const info = await result.toFile(resolvedOutput); + + return { + path: resolvedOutput, + width: info.width, + height: info.height, + format: info.format, + size: info.size, + }; +} + +export async function thumbnail( + input: string, + output: string, + size: ThumbnailSize = 'medium' +): Promise { + const dimension = THUMBNAIL_SIZES[size]; + return resize(input, output, dimension, dimension, 'cover'); +} diff --git a/src/index.ts b/src/index.ts index 6523312..3a9db3a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,18 @@ export { ClaudeBrowser } from './browser.js'; export { BrowserServer, startServer } from './server.js'; +export { + createFavicon, + convert, + resize, + crop, + compress, + thumbnail, + type FaviconResult, + type ImageResult, + type FitType, + type FormatType, + type ThumbnailSize, +} from './image.js'; export type { BrowserOptions, BrowserCommand, @@ -21,5 +34,11 @@ export type { NewPageCommand, CloseCommand, EvalCommand, + FaviconCommand, + ConvertCommand, + ResizeCommand, + CropCommand, + CompressCommand, + ThumbnailCommand, } from './types.js'; export type { ServerOptions } from './server.js'; diff --git a/src/mcp.ts b/src/mcp.ts index 4174b54..1fcfe27 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -3,6 +3,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; import { ClaudeBrowser } from './browser.js'; +import * as image from './image.js'; import { type CommandLike, type ResultLike, stderrLogger as log } from './logger.js'; const browser = new ClaudeBrowser({ headless: true, width: 1280, height: 800 }); @@ -187,6 +188,108 @@ server.tool( }) ); +// Image processing +server.tool( + 'favicon', + 'Generate a complete favicon set from an image (16x16, 32x32, 48x48, apple-touch-icon 180x180, android-chrome 192x192 and 512x512)', + { + input: z.string().describe('Path to source image'), + outputDir: z.string().describe('Directory to output favicon files'), + }, + withLogging('favicon', async ({ input, outputDir }) => { + const result = await image.createFavicon(input, outputDir); + return textResult( + JSON.stringify({ ok: true, files: result.files, outputDir: result.outputDir }) + ); + }) +); + +server.tool( + 'convert', + 'Convert an image to a different format (png, jpeg, webp, avif)', + { + input: z.string().describe('Path to source image'), + output: z.string().describe('Path for output image'), + format: z.enum(['png', 'jpeg', 'webp', 'avif']).describe('Target format'), + }, + withLogging('convert', async ({ input, output, format }) => { + const result = await image.convert(input, output, format); + return textResult(JSON.stringify({ ok: true, ...result })); + }) +); + +server.tool( + 'resize', + 'Resize an image to specified dimensions', + { + input: z.string().describe('Path to source image'), + output: z.string().describe('Path for output image'), + width: z.number().describe('Target width in pixels'), + height: z + .number() + .optional() + .describe('Target height in pixels (optional, maintains aspect ratio if omitted)'), + fit: z + .enum(['cover', 'contain', 'fill', 'inside', 'outside']) + .optional() + .default('cover') + .describe('How to fit the image'), + }, + withLogging('resize', async ({ input, output, width, height, fit }) => { + const result = await image.resize(input, output, width, height, fit); + return textResult(JSON.stringify({ ok: true, ...result })); + }) +); + +server.tool( + 'crop', + 'Crop a region from an image', + { + input: z.string().describe('Path to source image'), + output: z.string().describe('Path for output image'), + left: z.number().describe('Left edge position in pixels'), + top: z.number().describe('Top edge position in pixels'), + width: z.number().describe('Width of crop region in pixels'), + height: z.number().describe('Height of crop region in pixels'), + }, + withLogging('crop', async ({ input, output, left, top, width, height }) => { + const result = await image.crop(input, output, left, top, width, height); + return textResult(JSON.stringify({ ok: true, ...result })); + }) +); + +server.tool( + 'compress', + 'Compress an image to reduce file size', + { + input: z.string().describe('Path to source image'), + output: z.string().describe('Path for output image'), + quality: z.number().min(1).max(100).optional().default(80).describe('Quality level 1-100'), + }, + withLogging('compress', async ({ input, output, quality }) => { + const result = await image.compress(input, output, quality); + return textResult(JSON.stringify({ ok: true, ...result })); + }) +); + +server.tool( + 'thumbnail', + 'Create a thumbnail from an image', + { + input: z.string().describe('Path to source image'), + output: z.string().describe('Path for output image'), + size: z + .enum(['small', 'medium', 'large']) + .optional() + .default('medium') + .describe('Thumbnail size preset (small=150px, medium=300px, large=600px)'), + }, + withLogging('thumbnail', async ({ input, output, size }) => { + const result = await image.thumbnail(input, output, size); + return textResult(JSON.stringify({ ok: true, ...result })); + }) +); + // Start server async function main(): Promise { const transport = new StdioServerTransport(); diff --git a/src/types.ts b/src/types.ts index 1ad2499..8e79e36 100644 --- a/src/types.ts +++ b/src/types.ts @@ -77,6 +77,53 @@ export interface EvalCommand { script: string; } +// Image processing commands +export interface FaviconCommand { + cmd: 'favicon'; + input: string; + outputDir: string; +} + +export interface ConvertCommand { + cmd: 'convert'; + input: string; + output: string; + format: 'png' | 'jpeg' | 'webp' | 'avif'; +} + +export interface ResizeCommand { + cmd: 'resize'; + input: string; + output: string; + width: number; + height?: number; + fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside'; +} + +export interface CropCommand { + cmd: 'crop'; + input: string; + output: string; + left: number; + top: number; + width: number; + height: number; +} + +export interface CompressCommand { + cmd: 'compress'; + input: string; + output: string; + quality?: number; +} + +export interface ThumbnailCommand { + cmd: 'thumbnail'; + input: string; + output: string; + size?: 'small' | 'medium' | 'large'; +} + export type BrowserCommand = | GotoCommand | ClickCommand @@ -91,7 +138,13 @@ export type BrowserCommand = | WaitCommand | NewPageCommand | CloseCommand - | EvalCommand; + | EvalCommand + | FaviconCommand + | ConvertCommand + | ResizeCommand + | CropCommand + | CompressCommand + | ThumbnailCommand; // Response types export interface SuccessResponse { @@ -103,6 +156,13 @@ export interface SuccessResponse { count?: number; elements?: ElementInfo[]; result?: unknown; + // Image processing fields + files?: string[]; + outputDir?: string; + width?: number; + height?: number; + format?: string; + size?: number; } export interface ErrorResponse {