One .zip per skill. Inside: SKILL.md with the intent and the frontmatter, and the code that runs in the page.
Settings → Personalize → Skills. This is Cowork's own skill library, shared across every chat.
Click Add and choose the file you just downloaded. Cowork installs it once; no CLAUDE.md, no per-project setup.
The skill stays available in every session. Cowork runs it in your own Chrome, riding the session you are already signed into.
Search eBay listings by keyword, returning title, price, condition and item link.
{"query":"string, REQUIRED (search text, e.g. \"nintendo switch\")","page":"int (optional, default 1)"}{"count":"int","query":"string","page":"int","results":"array of { itemId, title, url, price, condition, shipping, attributes (every secondary row on the card, raw) }"}Searches listings by keyword, deduped by item id and with eBay's filler cards removed. Resolves in a single bridge call: the extractor fetches the page same-origin and parses it, so the params actually drive the request and no page is opened.
The extractor lives as text in the site's localStorage (key ebay-search-listings) and is rebuilt on each call with synchronous eval. It is stored once and then persists across navigation and browser restarts, so on every run except the first it is already there.
Default action: just call (step 1). The call itself reports NEEDS_STORE when the extractor is missing or out of date, only then do you store it (step 2) and call again.
Versioning. The stored extractor is gated by VER (must match the version in the frontmatter above). Both snippets carry VER = "1", keep them in sync with the frontmatter. When evaluate.js changes, bump version and VER in both snippets: the next call sees the mismatch, returns NEEDS_STORE, and re-stores automatically. Storage stays at two keys (ebay-search-listings and ebay-search-listings:ver), overwritten each store, versions never accumulate.
await (() => {
const KEY = "ebay-search-listings", VER = "1";
const s = localStorage.getItem(KEY);
if (!s || localStorage.getItem(KEY + ":ver") !== VER) return 'NEEDS_STORE';
const fn = eval('(' + s + ')');
return fn(document, { query: "nintendo switch" });
})()
Returns the matching listings (~60 per page). If it returns the string 'NEEDS_STORE', the extractor is missing or stale in this browser → do step 2 once, then run this exact call again.
Run this snippet verbatim. Never add an await before the eval, only the outer await is allowed.
Any ebay.com tab works, the extractor fetches what it needs and never reads the open page. The tab only supplies the origin and the session.
NEEDS_STORE; persists until localStorage is cleared)From any ebay.com tab, evaluate the following with the bridge, replacing <FN> with the contents of evaluate.js (the ( async (root, params) => { ... } ) function). Keep VER equal to the call snippet's:
(() => {
const KEY = "ebay-search-listings", VER = "1";
const fn = (<FN>);
localStorage.setItem(KEY, String(fn));
localStorage.setItem(KEY + ":ver", VER);
return 'stored';
})()
Returns "stored". The extractor passes through the model only here, once. Then go back to step 1.
eBay pads the grid with "Shop on eBay" placeholders pointing at a dummy /itm/123456; those are dropped, as are duplicate item ids.
Card titles carry a hidden accessibility span ("Opens in a new window or tab", localised per session) glued to the real title. The extractor reads the styled span instead of string-matching that suffix, so titles stay clean in any language.
shipping is one of several identical .s-card__attribute-row nodes and can only be told apart by its text, which is localised, it is matched for the common languages, and every row is also returned raw in attributes, so nothing is lost when the session speaks another one.
Text and currency follow the session, not this skill: the same call returns $ from a US session and ARS/Spanish from an Argentine one (verified). Prices come back as display strings exactly as ebay.com renders them, parse them against the currency you see, and never assume USD.
NEEDS_STORE → extractor missing or version-mismatched in this browser; run step 2 once, then retry step 1.missing_params: query is required → pass a search text.blocked → the site served a bot check instead of content. Open ebay.com in the tab, clear the check by hand, and retry.tool-broken → the markup changed; the selectors need recapturing.request_failed → no ebay.com tab open, or the call ran cross-origin.{ "type": "json", "jsonPath": "count" }