diff --git a/skills/amazon-shopping/.gitignore b/skills/amazon-shopping/.gitignore new file mode 100644 index 0000000..a8ee856 --- /dev/null +++ b/skills/amazon-shopping/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +coverage/ +*.log +tmp/ +out/ +*.real.html diff --git a/skills/amazon-shopping/SKILL.md b/skills/amazon-shopping/SKILL.md new file mode 100644 index 0000000..36e6e79 --- /dev/null +++ b/skills/amazon-shopping/SKILL.md @@ -0,0 +1,38 @@ +--- +name: amazon-shopping +description: Search amazon.com shopping results with product filters using the local web-automation skill. Use when the user asks to find, compare, filter, or summarize Amazon products by description, price, delivery, specs, review count, star rating, or star distribution. +--- + +# Amazon Shopping + +Use this skill for read-only Amazon product discovery and comparison. + +## First Checks + +Verify the browser dependency before live use: + +```bash +openclaw skills info web-automation +cd ~/.openclaw/workspace/skills/web-automation/scripts +node check-install.js +``` + +## Search Products + +Run the helper from the installed skill directory: + +```bash +cd ~/.openclaw/workspace/skills/amazon-shopping +scripts/search-products "" --json +``` + +Default to at most 15 products unless the user asks for a different count. For requested counts above 30, ask before continuing or split the request into batches. Always include source URLs, report missing fields explicitly, and do not claim review histogram data unless it was visible and extracted. + +## Guardrails + +This skill is for operator-directed, read-only product research. Before live scraping, the helper checks Amazon robots directives for planned paths. Do not automate sign-in, cart, purchase, wishlist, review submission, review-page crawling, CAPTCHA bypass, or blocked-page bypass. If Amazon returns a challenge or block page, stop and report that status. + +Read references when needed: +- `references/amazon-data-map.md` for fields and selectors. +- `references/web-automation-prompts.md` for browser extraction prompts. +- `references/compliance-and-failure-modes.md` for blocked-page and unknown-field behavior. diff --git a/skills/amazon-shopping/package-lock.json b/skills/amazon-shopping/package-lock.json new file mode 100644 index 0000000..866a352 --- /dev/null +++ b/skills/amazon-shopping/package-lock.json @@ -0,0 +1,610 @@ +{ + "name": "amazon-shopping-scripts", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "amazon-shopping-scripts", + "version": "1.0.0", + "dependencies": { + "minimist": "^1.2.8" + }, + "devDependencies": { + "@types/minimist": "^1.2.5", + "@types/node": "^24.8.1", + "tsx": "^4.20.6", + "typescript": "^5.9.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/skills/amazon-shopping/package.json b/skills/amazon-shopping/package.json new file mode 100644 index 0000000..6f9c400 --- /dev/null +++ b/skills/amazon-shopping/package.json @@ -0,0 +1,21 @@ +{ + "name": "amazon-shopping-scripts", + "version": "1.0.0", + "description": "Amazon shopping helper CLI for OpenClaw skills", + "type": "module", + "scripts": { + "amazon-shopping": "tsx src/cli.ts", + "lint": "node scripts/lint.mjs", + "test": "node --import tsx --test tests/*.test.ts", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "minimist": "^1.2.8" + }, + "devDependencies": { + "@types/minimist": "^1.2.5", + "@types/node": "^24.8.1", + "tsx": "^4.20.6", + "typescript": "^5.9.3" + } +} diff --git a/skills/amazon-shopping/references/amazon-data-map.md b/skills/amazon-shopping/references/amazon-data-map.md new file mode 100644 index 0000000..a10f36a --- /dev/null +++ b/skills/amazon-shopping/references/amazon-data-map.md @@ -0,0 +1,47 @@ +# Amazon Data Map + +Use this reference when deciding which visible Amazon fields can be reported by `amazon-shopping`. + +## Product Search Fields + +Search result cards should be treated as candidates, not final truth. Prefer cards with a non-empty `data-asin` value. Extract only visible data from the rendered search page: + +| Output field | Search-page source | Notes | +|---|---|---| +| `asin` | `data-asin` on result card | Required for normalized detail links. | +| `title` | product heading or product link text | Trim sponsored/accessibility boilerplate. | +| `url` | product link | Normalize to `https://www.amazon.com/dp/` when safe. | +| `imageUrl` | visible product image `src` | Optional. | +| `price` | visible `.a-price` text | Do not infer absent prices from snippets. | +| `rating` | `aria-label` or visible text like `4.6 out of 5 stars` | Optional. | +| `reviewCount` | text near rating like `1,234 ratings` | Optional. | +| `delivery.display` | visible delivery promise text | Optional and ZIP/session dependent. | +| `isSponsored` | visible sponsored marker | Sponsored results may be included but must be labeled. | + +## Detail Page Fields + +Open only normalized product detail URLs under `/dp/` or `/gp/product/`. Extract visible fields: + +| Output field | Detail-page source | Notes | +|---|---|---| +| `title` | `#productTitle` or equivalent heading | Detail title can replace search title. | +| `price` | buy-box/current price selectors | Variant pages can omit price. | +| `delivery` | delivery message near buy box | Report as text, not guaranteed. | +| `availability` | availability block | Optional. | +| `seller` | seller/ships-from visible text | Optional. | +| `bullets` | feature bullets list | Trim empty and hidden items. | +| `specs` | product overview/details/technical tables | Preserve name/value pairs. | +| `starBreakdown` | visible customer-review histogram | Percent or count basis only. Do not crawl review pages. | + +## Filter Semantics + +- `over 200 reviews` means `reviewCount > 200`. +- `at least 200 reviews` means `reviewCount >= 200`. +- `more than 4.5 stars` means `rating > 4.5`. +- `4.5 stars or better` means `rating >= 4.5`. +- `less than $4 each` means visible unit price first, then high-confidence unit-count inference. Unknown unit prices do not pass strict unit-price filters. +- Missing fields must be represented as `null` or noted in `missingFields` / `extractionNotes`; never fabricate values. + +## Official Alternatives + +Amazon Business Product Search API and Product Advertising API are official API paths for structured product data when the operator has credentials. This skill uses bounded web automation because the current install request requires `web-automation` scraping. diff --git a/skills/amazon-shopping/references/compliance-and-failure-modes.md b/skills/amazon-shopping/references/compliance-and-failure-modes.md new file mode 100644 index 0000000..96115ed --- /dev/null +++ b/skills/amazon-shopping/references/compliance-and-failure-modes.md @@ -0,0 +1,39 @@ +# Compliance And Failure Modes + +This reference is operational guidance, not legal advice. The operator is responsible for making sure a run complies with Amazon terms, robots directives, local law, and account obligations. + +## Required Guardrails + +- Fetch and evaluate `https://www.amazon.com/robots.txt` before live scraping planned Amazon paths. +- Stop if the effective rules disallow the planned search or detail paths. +- Do not automate sign-in, checkout, cart, wishlist, review submission, customer-review pages, reviewer profiles, or any disallowed path. +- Do not bypass CAPTCHA, bot checks, blocked pages, or access-denied pages. +- Do not print cookies, profile state, session storage, or account/location-specific browser data. + +## Allowed Scope + +Allowed behavior is bounded read-only product research over search result pages and normalized product detail pages: + +- `/s?k=` search results. +- `/dp/` product details. +- `/gp/product/` product details. + +Review data is limited to visible summary ratings/counts and visible histogram rows on search/detail pages. Do not navigate to `/product-reviews`, `/review`, `/gp/customer-reviews`, or review AJAX endpoints. + +## Failure Modes + +Return a structured warning and do not claim success when any of these happen: + +- CAPTCHA or bot-check page. +- Sign-in wall. +- HTTP 429 or 503 that remains after the bounded retry budget. +- Robots rules disallow a planned path. +- Product markup changes enough that required fields cannot be found. +- Amazon returns localized, personalized, or ZIP/session-dependent delivery text that cannot be verified. + +## Output Rules + +- Unknown fields stay unknown. +- Partial extraction is acceptable only when the response includes warnings and missing-field notes. +- Sponsored products can be returned by default but must be labeled. +- Counts above 30 require operator confirmation or batch splitting. diff --git a/skills/amazon-shopping/references/web-automation-prompts.md b/skills/amazon-shopping/references/web-automation-prompts.md new file mode 100644 index 0000000..6b05b1d --- /dev/null +++ b/skills/amazon-shopping/references/web-automation-prompts.md @@ -0,0 +1,27 @@ +# Web-Automation Prompts + +Use these patterns when debugging or extending the `amazon-shopping` browser workflow. The TypeScript helper is the default interface; these prompts document the intended rendered-page behavior. + +## Search Page + +```text +Use the installed web-automation skill. Open https://www.amazon.com/s?k=. Wait for rendered search results. If a CAPTCHA, bot check, sign-in wall, or access denied page appears, stop and return a challenge status. Otherwise extract visible product cards with ASIN, title, product URL, displayed price, rating text, review count text, delivery text, sponsor marker, and image URL. Do not open cart, sign-in, wishlist, or review-listing pages. +``` + +## Detail Page + +```text +For each candidate product detail URL under /dp/ or /gp/product/, open the page slowly one at a time. Extract title, canonical link, visible buy-box/current price, delivery summary, availability, seller when visible, feature bullets, product details/specification table rows, rating score, review count, and visible customer-review histogram percentages if present on the detail page. Do not navigate to /product-reviews or reviewer profile pages. +``` + +## Pagination + +```text +Follow only the visible Amazon pagination control for the next search page, or construct page= only after the current page exposes normal search results and no challenge/block. Stop when enough candidates have been collected, no next page exists, a challenge appears, or maxSearchPages is reached. +``` + +## Robustness Notes + +- Prefer Playwright locator/actionability behavior and bounded waits over fixed sleeps. +- Never follow sponsored redirect URLs, sign-in links, cart links, wishlist links, or review-page links. +- Return partial results with warnings when Amazon markup changes or fields are hidden. diff --git a/skills/amazon-shopping/scripts/lint.mjs b/skills/amazon-shopping/scripts/lint.mjs new file mode 100755 index 0000000..e949e1d --- /dev/null +++ b/skills/amazon-shopping/scripts/lint.mjs @@ -0,0 +1,54 @@ +#!/usr/bin/env node + +import { readdir, readFile, stat } from "node:fs/promises"; +import { join, relative } from "node:path"; + +const root = new URL("..", import.meta.url).pathname; +const scannedExtensions = new Set([".md", ".json", ".ts", ".js", ".mjs", ".sh"]); +const installSpecificPath = ["", "Users", "stefano"].join("/"); +const forbidden = [ + { + pattern: installSpecificPath, + message: "Source files must not hardcode this install path" + } +]; + +function extensionOf(path) { + const dot = path.lastIndexOf("."); + return dot === -1 ? "" : path.slice(dot); +} + +async function walk(dir) { + const entries = await readdir(dir); + const files = []; + for (const entry of entries) { + if (["node_modules", "dist", "coverage", "tmp", "out"].includes(entry)) { + continue; + } + const path = join(dir, entry); + const info = await stat(path); + if (info.isDirectory()) { + files.push(...await walk(path)); + } else if (scannedExtensions.has(extensionOf(path)) || entry === "SKILL.md") { + files.push(path); + } + } + return files; +} + +const failures = []; +for (const file of await walk(root)) { + const text = await readFile(file, "utf8"); + for (const rule of forbidden) { + if (text.includes(rule.pattern)) { + failures.push(`${relative(root, file)}: ${rule.message}`); + } + } +} + +if (failures.length > 0) { + for (const failure of failures) { + console.error(failure); + } + process.exitCode = 1; +} diff --git a/skills/amazon-shopping/scripts/search-products b/skills/amazon-shopping/scripts/search-products new file mode 100755 index 0000000..412218d --- /dev/null +++ b/skills/amazon-shopping/scripts/search-products @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SKILL_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +TSX="${SKILL_DIR}/node_modules/.bin/tsx" + +if [ ! -x "$TSX" ]; then + echo "Missing local dependencies. Run: cd \"$SKILL_DIR\" && npm install" >&2 + exit 127 +fi + +exec "$TSX" "$SKILL_DIR/src/cli.ts" "$@" diff --git a/skills/amazon-shopping/scripts/setup.sh b/skills/amazon-shopping/scripts/setup.sh new file mode 100755 index 0000000..9e28a65 --- /dev/null +++ b/skills/amazon-shopping/scripts/setup.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SKILL_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" + +cd "$SKILL_DIR" +npm install +npm run lint +npm test diff --git a/skills/amazon-shopping/src/cli.ts b/skills/amazon-shopping/src/cli.ts new file mode 100644 index 0000000..a2bd9f1 --- /dev/null +++ b/skills/amazon-shopping/src/cli.ts @@ -0,0 +1,177 @@ +#!/usr/bin/env node + +import minimist from "minimist"; +import { fileURLToPath } from "node:url"; + +import type { ProductFilters, SearchProductsRequest, SearchProductsResponse } from "./types.js"; + +export interface CliDeps { + stdout: Pick; + stderr: Pick; + now?: () => Date; + searchProducts?: (request: SearchProductsRequest) => Promise; +} + +export function usage(): string { + return `amazon-shopping + +Usage: + scripts/search-products "" [options] + scripts/search-products --query "" [options] + +Options: + --json Print JSON output + --markdown Print markdown output + --limit N Maximum products to return (default: 15) + --allow-large-limit Permit limits above 30 + --min-rating N Minimum rating score + --min-reviews N Minimum review count + --max-price N Maximum displayed product price + --max-unit-price N Maximum price per unit + --max-search-pages N Search result pages to scan, 1-5 (default: 2) + --skip-details Do not open product detail pages + --dry-run Parse and print the planned request without Amazon network access + --help Show this help +`; +} + +function parsePositiveInteger(value: unknown, name: string): number | undefined { + if (value === undefined) { + return undefined; + } + const parsed = Number(value); + if (!Number.isInteger(parsed) || parsed < 1) { + throw new Error(`${name} must be an integer greater than 0`); + } + return parsed; +} + +function parseNumber(value: unknown, name: string): number | undefined { + if (value === undefined) { + return undefined; + } + const parsed = Number(value); + if (!Number.isFinite(parsed)) { + throw new Error(`${name} must be a number`); + } + return parsed; +} + +export function buildSearchUrl(query: string): string { + return `https://www.amazon.com/s?k=${encodeURIComponent(query)}`; +} + +export function parseCliRequest(argv: string[]): SearchProductsRequest { + const args = minimist(argv, { + boolean: ["help", "json", "markdown", "allow-large-limit", "dry-run", "skip-details"], + string: [ + "query", + "limit", + "min-rating", + "min-reviews", + "max-price", + "max-unit-price", + "max-search-pages" + ], + alias: { h: "help" } + }); + + const query = String(args.query ?? args._.join(" ")).trim(); + if (!query) { + throw new Error("A product query is required"); + } + + const limit = parsePositiveInteger(args.limit, "limit") ?? 15; + if (limit > 30 && !args["allow-large-limit"]) { + throw new Error("Requested limits above 30 require --allow-large-limit or a batched run"); + } + + const maxSearchPages = parsePositiveInteger(args["max-search-pages"], "max-search-pages") ?? 2; + if (maxSearchPages > 5) { + throw new Error("max-search-pages must be 5 or less"); + } + + const filters: ProductFilters = { + includeKeywords: [], + excludeKeywords: [] + }; + const minRating = parseNumber(args["min-rating"], "min-rating"); + const minReviews = parsePositiveInteger(args["min-reviews"], "min-reviews"); + const maxPrice = parseNumber(args["max-price"], "max-price"); + const maxUnitPrice = parseNumber(args["max-unit-price"], "max-unit-price"); + if (minRating !== undefined) filters.minRating = minRating; + if (minReviews !== undefined) filters.minReviews = minReviews; + if (maxPrice !== undefined) filters.maxPrice = maxPrice; + if (maxUnitPrice !== undefined) filters.maxUnitPrice = maxUnitPrice; + + const json = Boolean(args.json); + const markdown = Boolean(args.markdown); + + return { + query, + filters, + limit, + maxSearchPages, + skipDetails: Boolean(args["skip-details"]), + dryRun: Boolean(args["dry-run"]), + output: json && markdown ? "both" : markdown ? "markdown" : "json" + }; +} + +function createDryRunResponse(request: SearchProductsRequest, now: () => Date): SearchProductsResponse { + return { + query: request.query, + filters: request.filters, + limit: request.limit, + maxSearchPages: request.maxSearchPages, + results: [], + filteredOutCount: 0, + warnings: [`Dry run only. Planned search URL: ${buildSearchUrl(request.query)}`], + source: { + site: "amazon.com", + scrapedAt: now().toISOString(), + automation: "web-automation/CloakBrowser" + } + }; +} + +async function defaultSearchProducts(request: SearchProductsRequest, deps: CliDeps): Promise { + if (request.dryRun) { + return createDryRunResponse(request, deps.now ?? (() => new Date())); + } + throw new Error("Live Amazon search is not implemented yet. Use --dry-run until browser orchestration is installed."); +} + +export async function runCli( + argv: string[], + deps: CliDeps = { stdout: process.stdout, stderr: process.stderr } +): Promise { + const rawArgs = minimist(argv, { boolean: ["help"], alias: { h: "help" } }); + if (rawArgs.help || argv.length === 0) { + deps.stdout.write(usage()); + return 0; + } + + try { + const request = parseCliRequest(argv); + const response = deps.searchProducts + ? await deps.searchProducts(request) + : await defaultSearchProducts(request, deps); + deps.stdout.write(`${JSON.stringify(response, null, 2)}\n`); + return 0; + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + deps.stderr.write(`${message}\n`); + return 1; + } +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { + runCli(process.argv.slice(2)).then((code) => { + process.exitCode = code; + }).catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + process.stderr.write(`${message}\n`); + process.exitCode = 1; + }); +} diff --git a/skills/amazon-shopping/src/types.ts b/skills/amazon-shopping/src/types.ts new file mode 100644 index 0000000..c86f804 --- /dev/null +++ b/skills/amazon-shopping/src/types.ts @@ -0,0 +1,84 @@ +export interface SearchProductsRequest { + query: string; + filters: ProductFilters; + limit: number; + maxSearchPages: number; + skipDetails: boolean; + dryRun: boolean; + output: "json" | "markdown" | "both"; +} + +export interface ProductFilters { + minRating?: number; + minReviews?: number; + maxPrice?: number; + maxUnitPrice?: number; + includeKeywords: string[]; + excludeKeywords: string[]; + requirePrime?: boolean; + requireFreeDelivery?: boolean; + deliveryBy?: string; +} + +export interface ProductSearchResult { + asin: string; + title: string; + url: string; + imageUrl?: string; + price?: MoneyValue; + unitPrice?: MoneyValue; + rating?: number; + reviewCount?: number; + starBreakdown?: StarBreakdown; + delivery?: DeliverySummary; + specs: ProductSpec[]; + bullets: string[]; + seller?: string; + isSponsored?: boolean; + availability?: string; + matchedFilters: string[]; + missingFields: string[]; + extractionNotes: string[]; +} + +export interface MoneyValue { + amount: number; + currency: "USD"; + display: string; +} + +export interface DeliverySummary { + display: string; + prime?: boolean; + free?: boolean; + fastestDate?: string; +} + +export interface StarBreakdown { + five?: number; + four?: number; + three?: number; + two?: number; + one?: number; + basis: "percent" | "count"; +} + +export interface ProductSpec { + name: string; + value: string; +} + +export interface SearchProductsResponse { + query: string; + filters: ProductFilters; + limit: number; + maxSearchPages: number; + results: ProductSearchResult[]; + filteredOutCount: number; + warnings: string[]; + source: { + site: "amazon.com"; + scrapedAt: string; + automation: "web-automation/CloakBrowser"; + }; +} diff --git a/skills/amazon-shopping/tests/cli.test.ts b/skills/amazon-shopping/tests/cli.test.ts new file mode 100644 index 0000000..5a3ebf2 --- /dev/null +++ b/skills/amazon-shopping/tests/cli.test.ts @@ -0,0 +1,94 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { buildSearchUrl, parseCliRequest, runCli } from "../src/cli.js"; + +function createOutput() { + let stdout = ""; + let stderr = ""; + return { + stdout: { write: (chunk: string) => { stdout += chunk; return true; } }, + stderr: { write: (chunk: string) => { stderr += chunk; return true; } }, + get stdoutText() { return stdout; }, + get stderrText() { return stderr; } + }; +} + +describe("amazon-shopping CLI", () => { + it("prints help", async () => { + const output = createOutput(); + const code = await runCli(["--help"], output); + + assert.equal(code, 0); + assert.match(output.stdoutText, /scripts\/search-products/); + assert.match(output.stdoutText, /--dry-run/); + }); + + it("defaults to 15 results and two search pages", () => { + const request = parseCliRequest(["usb c cable"]); + + assert.equal(request.query, "usb c cable"); + assert.equal(request.limit, 15); + assert.equal(request.maxSearchPages, 2); + assert.equal(request.output, "json"); + }); + + it("maps kebab-case CLI filters into the request contract", () => { + const request = parseCliRequest([ + "--query", + "100w led bulbs", + "--min-rating", + "4.5", + "--min-reviews", + "200", + "--max-unit-price", + "4", + "--max-search-pages", + "3", + "--skip-details", + "--dry-run" + ]); + + assert.equal(request.query, "100w led bulbs"); + assert.equal(request.filters.minRating, 4.5); + assert.equal(request.filters.minReviews, 200); + assert.equal(request.filters.maxUnitPrice, 4); + assert.equal(request.maxSearchPages, 3); + assert.equal(request.skipDetails, true); + assert.equal(request.dryRun, true); + }); + + it("maps output modes", () => { + assert.equal(parseCliRequest(["usb c cable", "--json"]).output, "json"); + assert.equal(parseCliRequest(["usb c cable", "--markdown"]).output, "markdown"); + assert.equal(parseCliRequest(["usb c cable", "--json", "--markdown"]).output, "both"); + }); + + it("rejects limits below one", () => { + assert.throws( + () => parseCliRequest(["usb c cable", "--limit", "0"]), + /limit must be an integer greater than 0/ + ); + }); + + it("rejects unsafe large limits unless explicitly allowed", () => { + assert.throws( + () => parseCliRequest(["usb c cable", "--limit", "31"]), + /require --allow-large-limit/ + ); + }); + + it("rejects search page caps above five", () => { + assert.throws( + () => parseCliRequest(["usb c cable", "--max-search-pages", "6"]), + /max-search-pages must be 5 or less/ + ); + }); + + it("builds the Amazon search URL without live network access", () => { + assert.equal( + buildSearchUrl("100w led bulbs"), + "https://www.amazon.com/s?k=100w%20led%20bulbs" + ); + }); +}); diff --git a/skills/amazon-shopping/tsconfig.json b/skills/amazon-shopping/tsconfig.json new file mode 100644 index 0000000..46bd145 --- /dev/null +++ b/skills/amazon-shopping/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "types": ["node"] + }, + "include": ["src/**/*.ts", "tests/**/*.ts"] +}