💬 Commit message: Update 2026-02-07 14:20:25, 8 files, 1060 lines

📁 Files changed: 8
📝 Lines changed: 1060

  • package-lock.json
  • package.json
  • browser.ts
  • cli.ts
  • image.ts
  • index.ts
  • mcp.ts
  • types.ts
This commit is contained in:
Adam Ladachowski
2026-02-07 14:20:25 +01:00
parent 4f0bec5f81
commit 67b6dc3a79
8 changed files with 1056 additions and 4 deletions
+548 -2
View File
@@ -13,7 +13,8 @@
"chalk": "^5.6.2", "chalk": "^5.6.2",
"express": "^5.2.1", "express": "^5.2.1",
"log-symbols": "^7.0.1", "log-symbols": "^7.0.1",
"playwright": "^1.41.0" "playwright": "^1.41.0",
"sharp": "^0.34.5"
}, },
"bin": { "bin": {
"claude-browse": "dist/cli.js", "claude-browse": "dist/cli.js",
@@ -23,6 +24,7 @@
"@biomejs/biome": "^1.9.0", "@biomejs/biome": "^1.9.0",
"@types/express": "^5.0.6", "@types/express": "^5.0.6",
"@types/node": "^20.11.0", "@types/node": "^20.11.0",
"@types/sharp": "^0.31.1",
"@types/supertest": "^6.0.3", "@types/supertest": "^6.0.3",
"@vitest/coverage-v8": "^4.0.18", "@vitest/coverage-v8": "^4.0.18",
"supertest": "^7.2.2", "supertest": "^7.2.2",
@@ -257,6 +259,16 @@
"node": ">=14.21.3" "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": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.27.3", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
@@ -711,6 +723,471 @@
"hono": "^4" "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": { "node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
@@ -1296,6 +1773,16 @@
"@types/node": "*" "@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": { "node_modules/@types/superagent": {
"version": "8.1.9", "version": "8.1.9",
"resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz",
@@ -1765,6 +2252,15 @@
"node": ">= 0.8" "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": { "node_modules/dezalgo": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
@@ -2924,7 +3420,6 @@
"version": "7.7.4", "version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
@@ -2984,6 +3479,50 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC" "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": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -3219,6 +3758,13 @@
"node": ">=0.6" "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": { "node_modules/type-is": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+3 -1
View File
@@ -52,12 +52,14 @@
"chalk": "^5.6.2", "chalk": "^5.6.2",
"express": "^5.2.1", "express": "^5.2.1",
"log-symbols": "^7.0.1", "log-symbols": "^7.0.1",
"playwright": "^1.41.0" "playwright": "^1.41.0",
"sharp": "^0.34.5"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.9.0", "@biomejs/biome": "^1.9.0",
"@types/express": "^5.0.6", "@types/express": "^5.0.6",
"@types/node": "^20.11.0", "@types/node": "^20.11.0",
"@types/sharp": "^0.31.1",
"@types/supertest": "^6.0.3", "@types/supertest": "^6.0.3",
"@vitest/coverage-v8": "^4.0.18", "@vitest/coverage-v8": "^4.0.18",
"supertest": "^7.2.2", "supertest": "^7.2.2",
+67
View File
@@ -1,5 +1,6 @@
import { resolve } from 'node:path'; import { resolve } from 'node:path';
import { type Browser, type BrowserContext, type Page, webkit } from 'playwright'; 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'; import type { BrowserCommand, BrowserOptions, CommandResponse, ElementInfo } from './types.js';
export class ClaudeBrowser { export class ClaudeBrowser {
@@ -190,6 +191,72 @@ export class ClaudeBrowser {
const result = await this.eval(cmd.script); const result = await this.eval(cmd.script);
return { ok: true, result }; 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: { default: {
const _exhaustive: never = cmd; const _exhaustive: never = cmd;
return { ok: false, error: `Unknown command: ${(_exhaustive as { cmd: string }).cmd}` }; return { ok: false, error: `Unknown command: ${(_exhaustive as { cmd: string }).cmd}` };
+59
View File
@@ -2,6 +2,7 @@
import { resolve } from 'node:path'; import { resolve } from 'node:path';
import { parseArgs } from 'node:util'; import { parseArgs } from 'node:util';
import { ClaudeBrowser } from './browser.js'; import { ClaudeBrowser } from './browser.js';
import * as image from './image.js';
import { startServer } from './server.js'; import { startServer } from './server.js';
import type { ElementInfo } from './types.js'; import type { ElementInfo } from './types.js';
@@ -21,6 +22,11 @@ const { values, positionals } = parseArgs({
type: { type: 'string', short: 't', multiple: true }, type: { type: 'string', short: 't', multiple: true },
help: { type: 'boolean', default: false }, help: { type: 'boolean', default: false },
version: { type: 'boolean', short: 'v', 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 -v, --version Show version
--help Show this help --help Show this help
Image Processing:
--favicon <dir> Generate favicon set to directory (from screenshot or input)
--convert <format> Convert screenshot to format (png, jpeg, webp, avif)
--resize <WxH> Resize screenshot (e.g., 800x600 or 800 for width only)
--compress <quality> Compress with quality 1-100
Examples: Examples:
claude-browse https://example.com claude-browse https://example.com
claude-browse -o page.png -w 1920 -h 1080 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 -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 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): Server mode (default):
claude-browse # Start server on port 13373 claude-browse # Start server on port 13373
claude-browse --headed # Start with visible browser 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":"url"}'
curl -X POST http://localhost:13373 -d '{"cmd":"html"}' curl -X POST http://localhost:13373 -d '{"cmd":"html"}'
curl -X POST http://localhost:13373 -d '{"cmd":"close"}' 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() { function getViewportConfig() {
@@ -157,10 +181,45 @@ async function runInteractiveMode(browser: ClaudeBrowser): Promise<void> {
await new Promise(() => {}); await new Promise(() => {});
} }
async function processImageOptions(screenshotPath: string): Promise<void> {
// 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<void> { async function runScreenshotMode(browser: ClaudeBrowser): Promise<void> {
const outputPath = resolve(values.output as string); const outputPath = resolve(values.output as string);
console.log(`Saving screenshot to: ${outputPath}`); console.log(`Saving screenshot to: ${outputPath}`);
await browser.screenshot(outputPath, values.fullpage); await browser.screenshot(outputPath, values.fullpage);
// Process any image options
await processImageOptions(outputPath);
await browser.close(); await browser.close();
console.log('Done!'); console.log('Done!');
} }
+196
View File
@@ -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<ThumbnailSize, number> = {
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<void> {
await mkdir(dirname(filePath), { recursive: true });
}
export async function createFavicon(input: string, outputDir: string): Promise<FaviconResult> {
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<ImageResult> {
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<ImageResult> {
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<ImageResult> {
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<ImageResult> {
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<ImageResult> {
const dimension = THUMBNAIL_SIZES[size];
return resize(input, output, dimension, dimension, 'cover');
}
+19
View File
@@ -1,5 +1,18 @@
export { ClaudeBrowser } from './browser.js'; export { ClaudeBrowser } from './browser.js';
export { BrowserServer, startServer } from './server.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 { export type {
BrowserOptions, BrowserOptions,
BrowserCommand, BrowserCommand,
@@ -21,5 +34,11 @@ export type {
NewPageCommand, NewPageCommand,
CloseCommand, CloseCommand,
EvalCommand, EvalCommand,
FaviconCommand,
ConvertCommand,
ResizeCommand,
CropCommand,
CompressCommand,
ThumbnailCommand,
} from './types.js'; } from './types.js';
export type { ServerOptions } from './server.js'; export type { ServerOptions } from './server.js';
+103
View File
@@ -3,6 +3,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod'; import { z } from 'zod';
import { ClaudeBrowser } from './browser.js'; import { ClaudeBrowser } from './browser.js';
import * as image from './image.js';
import { type CommandLike, type ResultLike, stderrLogger as log } from './logger.js'; import { type CommandLike, type ResultLike, stderrLogger as log } from './logger.js';
const browser = new ClaudeBrowser({ headless: true, width: 1280, height: 800 }); 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 // Start server
async function main(): Promise<void> { async function main(): Promise<void> {
const transport = new StdioServerTransport(); const transport = new StdioServerTransport();
+61 -1
View File
@@ -77,6 +77,53 @@ export interface EvalCommand {
script: string; 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 = export type BrowserCommand =
| GotoCommand | GotoCommand
| ClickCommand | ClickCommand
@@ -91,7 +138,13 @@ export type BrowserCommand =
| WaitCommand | WaitCommand
| NewPageCommand | NewPageCommand
| CloseCommand | CloseCommand
| EvalCommand; | EvalCommand
| FaviconCommand
| ConvertCommand
| ResizeCommand
| CropCommand
| CompressCommand
| ThumbnailCommand;
// Response types // Response types
export interface SuccessResponse { export interface SuccessResponse {
@@ -103,6 +156,13 @@ export interface SuccessResponse {
count?: number; count?: number;
elements?: ElementInfo[]; elements?: ElementInfo[];
result?: unknown; result?: unknown;
// Image processing fields
files?: string[];
outputDir?: string;
width?: number;
height?: number;
format?: string;
size?: number;
} }
export interface ErrorResponse { export interface ErrorResponse {