diff --git a/README.md b/README.md index 695a11d..7677476 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ This repository contains practical OpenClaw skills and companion integrations. I | Skill | What it does | Path | |---|---|---| -| `amazon-shopping` | Search Amazon.com product results with bounded web-automation, price/unit-price/width/Prime/delivery filters, specs, ratings, review metadata, and fixed markdown tables. | `skills/amazon-shopping` | +| `amazon-shopping` | Search Amazon.com product results with bounded web-automation, price/unit-price/width/Prime/delivery filters, specs, ratings, review metadata, and chat-safe result blocks with direct links. | `skills/amazon-shopping` | | `flight-finder` | Collect structured flight-search inputs, run bounded multi-source travel searches, rank results in USD, and gate fixed-template PDF/email delivery through Luke’s sender path. | `skills/flight-finder` | | `gitea-api` | Interact with Gitea via REST API (repos, issues, PRs, releases, branches, user info). | `skills/gitea-api` | | `nordvpn-client` | Install, log in to, connect, disconnect, and verify NordVPN sessions across Linux CLI and macOS NordLynx/WireGuard backends. | `skills/nordvpn-client` | diff --git a/docs/amazon-shopping.md b/docs/amazon-shopping.md index f1c4f12..6fa6864 100644 --- a/docs/amazon-shopping.md +++ b/docs/amazon-shopping.md @@ -87,17 +87,22 @@ Each result includes the product ASIN, title, source URL, price, unit price when Unknown or hidden fields stay unknown. The skill does not invent delivery dates, star histograms, prices, or review counts. -Markdown output uses a fixed table intended for direct user-facing answers: +Markdown output uses chat-safe result blocks intended for direct user-facing answers in WhatsApp, Telegram, and terminals. Each product must keep a direct URL line: ```markdown ## Best Matches -| # | Product | Price | Rating | Reviews | Width | Prime | Delivery | Link | -|---|---|---:|---:|---:|---:|---|---|---| -| 1 | Example Sofa Bed | $399.99 | 4.3 stars | 250 | 83" OK | Prime OK | FREE delivery Tomorrow OK | [Amazon](https://www.amazon.com/dp/ASIN) | +1. Example Sofa Bed +Price: $399.99 +Rating: 4.3 stars +Reviews: 250 +Width: 83" OK +Prime: Prime OK +Delivery: FREE delivery Tomorrow OK +Link: https://www.amazon.com/dp/ASIN ``` -The `OK` / `NO` marker is only attached to columns that correspond to requested filters. For example, `Prime OK` means the helper found a Prime signal on the search card or detail page; `not verified NO` means the product did not pass a requested Prime filter. +The `OK` / `NO` marker is only attached to fields that correspond to requested filters. For example, `Prime OK` means the helper found a Prime signal on the search card or detail page; `not verified NO` means the product did not pass a requested Prime filter. ## Filters @@ -123,6 +128,7 @@ Examples of supported natural-language filters: - `delivery by tomorrow` - `overnight shipping` - `top 10 by price` +- `rating 4.0 or better` Equivalent CLI flags: diff --git a/skills/amazon-shopping/SKILL.md b/skills/amazon-shopping/SKILL.md index 03f1d62..8ee6a56 100644 --- a/skills/amazon-shopping/SKILL.md +++ b/skills/amazon-shopping/SKILL.md @@ -28,7 +28,7 @@ Use single quotes around product requests that contain dollar amounts so the she 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. -For user-facing answers, use the generated markdown table as the presentation template. Keep the columns `#`, `Product`, `Price`, `Rating`, `Reviews`, `Width`, `Prime`, `Delivery`, and `Link` when those fields are relevant. Do not rewrite Prime or delivery status as verified unless the helper marks it verified. +For user-facing answers, use the generated chat-safe result blocks as the presentation template. Keep the direct `Link: https://...` line for every product because WhatsApp and Telegram do not reliably render markdown tables. Do not rewrite Prime or delivery status as verified unless the helper marks it verified. Supported filters include minimum rating, minimum reviews, maximum price, maximum unit price, minimum width in inches, Prime delivery, delivery by today/tomorrow/overnight, and sort by price. Natural language such as `77 inches or wider`, `shipped with Prime`, `delivery by tomorrow`, `overnight shipping`, and `top 10 by price` is parsed automatically. CLI flags are also available: `--min-width`, `--require-prime`, `--delivery-by`, and `--sort-by price`. diff --git a/skills/amazon-shopping/src/query-parser.ts b/skills/amazon-shopping/src/query-parser.ts index 024bc36..f90df34 100644 --- a/skills/amazon-shopping/src/query-parser.ts +++ b/skills/amazon-shopping/src/query-parser.ts @@ -4,6 +4,9 @@ function cleanQuery(text: string): string { return text .replace(/\breview score of\b/gi, " ") .replace(/\brating of\b/gi, " ") + .replace(/\b(?:delivery|shipping)\s+only\b/gi, " ") + .replace(/\blow\s+to\s+high\b/gi, " ") + .replace(/\bhigh\s+to\s+low\b/gi, " ") .replace(/\bof\s+in\s+width\b/gi, " ") .replace(/\bin\s+width\b/gi, " ") .replace(/\b(?:that|and|with|have)\b/gi, " ") @@ -89,7 +92,7 @@ export function parseNaturalLanguageRequest(input: string): ParsedNaturalLanguag } const exclusiveRating = remaining.match(/\b(?:a\s+)?(?:(?:review score|rating)\s+of\s+|rating\s+)?(?:more than|over|above|rated above)\s+([0-5](?:\.[0-9])?)\s*(?:stars?)?\b/i); - const inclusiveRating = remaining.match(/\b(?:review score|rating)\s+of\s+([0-5](?:\.[0-9])?)\s*(?:stars?)?\s+(?:or|and)\s+(?:higher|better)\b/i) + const inclusiveRating = remaining.match(/\b(?:a\s+)?(?:review score|rating)(?:\s+of)?\s+([0-5](?:\.[0-9])?)\s*(?:stars?)?\s+(?:or|and)\s+(?:higher|better)\b/i) ?? remaining.match(/\b([0-5](?:\.[0-9])?)\s*stars?\s+or\s+better\b/i) ?? remaining.match(/\b([0-5](?:\.[0-9])?)\s*stars?\s+(?:or|and)\s+(?:higher|better)\b/i) ?? remaining.match(/\b(?:at least|minimum|min\.?)\s+([0-5](?:\.[0-9])?)\s*(?:stars?|rating)?\b/i); diff --git a/skills/amazon-shopping/src/report.ts b/skills/amazon-shopping/src/report.ts index 996ded0..15e21ed 100644 --- a/skills/amazon-shopping/src/report.ts +++ b/skills/amazon-shopping/src/report.ts @@ -44,8 +44,8 @@ function formatFilters(filters: ProductFilters): string { return parts.length > 0 ? parts.join(", ") : "none"; } -function escapeCell(value: string): string { - return value.replace(/\|/g, "\\|").replace(/\s+/g, " ").trim(); +function compactText(value: string): string { + return value.replace(/\s+/g, " ").trim(); } function marker(passes: boolean | undefined, enabled: boolean): string { @@ -82,24 +82,18 @@ function deliveryCell(product: ProductSearchResult, filters: ProductFilters): st return `${display}${marker(passes, true)}`; } -function resultTable(products: ProductSearchResult[], filters: ProductFilters): string[] { - const rows = [ - "| # | Product | Price | Rating | Reviews | Width | Prime | Delivery | Link |", - "|---|---|---:|---:|---:|---:|---|---|---|", - ...products.map((product, index) => [ - String(index + 1), - escapeCell(product.title), - product.price?.display ?? "unknown", - `${product.rating ?? "unknown"} stars`, - product.reviewCount?.toLocaleString("en-US") ?? "unknown", - widthCell(product, filters), - primeCell(product, filters), - escapeCell(deliveryCell(product, filters)), - `[Amazon](${product.url})` - ].join(" | ")) - .map((row) => `| ${row} |`) - ]; - return rows; +function resultBlocks(products: ProductSearchResult[], filters: ProductFilters): string[] { + return products.flatMap((product, index) => [ + `${index + 1}. ${compactText(product.title)}`, + `Price: ${product.price?.display ?? "unknown"}`, + `Rating: ${product.rating ?? "unknown"} stars`, + `Reviews: ${product.reviewCount?.toLocaleString("en-US") ?? "unknown"}`, + `Width: ${widthCell(product, filters)}`, + `Prime: ${primeCell(product, filters)}`, + `Delivery: ${compactText(deliveryCell(product, filters))}`, + `Link: ${product.url}`, + "" + ]); } function metadataLines(products: ProductSearchResult[]): string[] { @@ -129,7 +123,7 @@ export function createMarkdownReport(response: SearchProductsResponse): string { "## Best Matches", "", response.results.length > 0 ? "" : "No products matched all requested filters.", - ...resultTable(response.results, response.filters), + ...resultBlocks(response.results, response.filters), "", ...metadataLines(response.results) ].filter((line) => line !== ""); diff --git a/skills/amazon-shopping/tests/query-parser.test.ts b/skills/amazon-shopping/tests/query-parser.test.ts index a1bb01f..b21cd14 100644 --- a/skills/amazon-shopping/tests/query-parser.test.ts +++ b/skills/amazon-shopping/tests/query-parser.test.ts @@ -35,6 +35,22 @@ describe("parseNaturalLanguageRequest", () => { assert.equal(parsed.filters.minRating, 4); }); + it("extracts rating filters without requiring the word of", () => { + const parsed = parseNaturalLanguageRequest( + "sofa bed, 77 inches or wider, over 50 reviews, rating 4.0 or better, Prime delivery only, sort by price low to high" + ); + + assert.equal(parsed.query, "sofa bed"); + assert.equal(parsed.filters.minWidthInches, 77); + assert.equal(parsed.filters.widthComparison, "gte"); + assert.equal(parsed.filters.minReviews, 50); + assert.equal(parsed.filters.reviewCountComparison, "gt"); + assert.equal(parsed.filters.minRating, 4); + assert.equal(parsed.filters.ratingComparison, "gte"); + assert.equal(parsed.filters.requirePrime, true); + assert.equal(parsed.filters.sortBy, "price"); + }); + it("extracts limit and max product price phrases", () => { const parsed = parseNaturalLanguageRequest("return 5 wireless mouse under $30"); diff --git a/skills/amazon-shopping/tests/report.test.ts b/skills/amazon-shopping/tests/report.test.ts index c2ba312..21c431e 100644 --- a/skills/amazon-shopping/tests/report.test.ts +++ b/skills/amazon-shopping/tests/report.test.ts @@ -53,7 +53,7 @@ describe("report", () => { assert.match(markdown, /https:\/\/www\.amazon\.com\/dp\/B0TEST0001/); }); - it("creates a table template with constraint status markers", () => { + it("creates a chat-safe template with direct product links and constraint status markers", () => { const markdown = createMarkdownReport(createResponse({ query: "sofa bed beige", filters: { @@ -87,11 +87,13 @@ describe("report", () => { })); assert.match(markdown, /## Best Matches/); - assert.match(markdown, /\| # \| Product \| Price \| Rating \| Reviews \| Width \| Prime \| Delivery \| Link \|/); + assert.doesNotMatch(markdown, /\| # \| Product \| Price \| Rating \| Reviews \| Width \| Prime \| Delivery \| Link \|/); + assert.doesNotMatch(markdown, /\[Amazon\]\(https:\/\/www\.amazon\.com\/dp\/B0SOFABED1\)/); assert.match(markdown, /HONBAY Modular Sectional Sleeper/); + assert.match(markdown, /Link: https:\/\/www\.amazon\.com\/dp\/B0SOFABED1/); assert.match(markdown, /83\.4" OK/); assert.match(markdown, /Prime OK/); assert.match(markdown, /Tomorrow OK/); - assert.match(markdown, /\[Amazon\]\(https:\/\/www\.amazon\.com\/dp\/B0SOFABED1\)/); + assert.match(markdown, /Price: \$539\.99/); }); });