Skip to content

Commit 66ff64d

Browse files
committed
feat(tools): ebay + npm tools (T-0016, T-0017)
1 parent 70e33c6 commit 66ff64d

7 files changed

Lines changed: 386 additions & 0 deletions

File tree

docs/stories/050-tool-ebay.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# 050 - Tool: ebay
2+
3+
**Persona:** [CLI User](../personas/cli-user.md),
4+
[AI Coding Assistant](../personas/ai-coding-assistant.md)
5+
6+
## Goal
7+
8+
Search eBay listings and extract pricing data — including sold/completed prices —
9+
from the CLI without a browser.
10+
11+
## Stories
12+
13+
- As a CLI user, I run `ibr tool ebay --param query="iphone 15 pro"` to find
14+
current listings with title, price, condition, and URL.
15+
- As a CLI user, I pass `--param sold=true` to see only sold/completed listings
16+
for accurate market-price research.
17+
- As a CLI user, I pass `--param country=co.uk` to search the UK eBay site.
18+
- As a CLI user, I pass `--param max_results=10` to get more results.
19+
20+
## Acceptance Criteria
21+
22+
- `ibr tool ebay --param query=<query>` exits 0 and returns listings.
23+
- Each listing includes: title, price, condition, sold status, listing URL.
24+
- `sold` defaults to `false`; passing `--param sold=true` appends
25+
`LH_Sold=1&LH_Complete=1` to the eBay URL.
26+
- `country` defaults to `com`; other TLDs (co.uk, de, etc.) produce the correct
27+
domain.
28+
- `max_results` defaults to `5`.
29+
- Missing `query` → non-zero exit with "Missing required param: query".
30+
31+
## E2E Coverage
32+
33+
- `test/e2e/cli-tool-vcr.test.js` — VCR: exits 0, no config errors;
34+
required param validation.

docs/stories/051-tool-npm.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# 051 - Tool: npm
2+
3+
**Persona:** [CLI User](../personas/cli-user.md),
4+
[AI Coding Assistant](../personas/ai-coding-assistant.md)
5+
6+
## Goal
7+
8+
Fetch npm package metadata — version, downloads, license, dependents — from
9+
the CLI without leaving the terminal.
10+
11+
## Stories
12+
13+
- As a CLI user, I run `ibr tool npm --param package=react` to get the latest
14+
version, weekly downloads, license, and dependent count for React.
15+
- As an AI coding assistant, I call this tool to check a package's health
16+
(downloads, dependents, license) before recommending it.
17+
18+
## Acceptance Criteria
19+
20+
- `ibr tool npm --param package=<name>` exits 0 and returns package metadata.
21+
- Output includes: name, version, description, weekly_downloads, license,
22+
homepage, github repo URL, dependents count.
23+
- Missing `package` → non-zero exit with "Missing required param: package".
24+
25+
## E2E Coverage
26+
27+
- `test/e2e/cli-tool-vcr.test.js` — VCR: exits 0, no config errors;
28+
required param validation.

test/e2e/cassettes/tool-ebay.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[
2+
"{\"url\":\"{SERVER_URL}/tool-results.html\",\"instructions\":[{\"name\":\"extract\",\"prompt\":\"extract eBay listing data with title, price, condition, sold status, and URL\"}]}",
3+
"[{\"title\":\"iPhone 15 Pro 256GB Black Titanium\",\"price\":\"$899.00\",\"condition\":\"Used\",\"sold\":true,\"url\":\"https://www.ebay.com/itm/123456789\"},{\"title\":\"iPhone 15 Pro 128GB Natural Titanium\",\"price\":\"$849.99\",\"condition\":\"Used\",\"sold\":true,\"url\":\"https://www.ebay.com/itm/987654321\"}]"
4+
]

test/e2e/cassettes/tool-npm.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[
2+
"{\"url\":\"{SERVER_URL}/tool-results.html\",\"instructions\":[{\"name\":\"extract\",\"prompt\":\"extract npm package metadata: name, version, description, weekly downloads, license, homepage, github repo, and dependents count\"}]}",
3+
"{\"name\":\"react\",\"version\":\"18.3.1\",\"description\":\"React is a JavaScript library for building user interfaces.\",\"weekly_downloads\":\"24500000\",\"license\":\"MIT\",\"homepage\":\"https://reactjs.org\",\"github\":\"https://github.com/facebook/react\",\"dependents\":\"108234\"}"
4+
]

test/e2e/cli-tool-vcr.test.js

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,3 +232,275 @@ describe('ibr tool context7 (T-0005)', () => {
232232
expect(result.stdout + result.stderr).toMatch(/question|required param/i);
233233
}, 10000);
234234
});
235+
236+
// ─── ebay (T-0016) ───────────────────────────────────────────────────────────
237+
238+
describe('ibr tool ebay (T-0016)', () => {
239+
let ai, web;
240+
241+
beforeAll(async () => {
242+
web = await startStaticServer();
243+
ai = await startFromCassette('tool-ebay', { SERVER_URL: web.baseUrl });
244+
}, 15000);
245+
246+
afterAll(async () => {
247+
await ai.close();
248+
await web.close();
249+
});
250+
251+
it('exits 0 and completes extraction without error', async () => {
252+
const result = await runIbr(
253+
['tool', 'ebay', '--param', 'query=iphone 15 pro'],
254+
{ ...BASE_ENV, OPENAI_BASE_URL: ai.baseUrl },
255+
);
256+
expect(result.code).toBe(0);
257+
expect(result.stdout + result.stderr).toMatch(/Task execution completed/i);
258+
}, 30000);
259+
260+
it('query is required — missing → non-zero exit before browser', async () => {
261+
const result = await runIbr(
262+
['tool', 'ebay'],
263+
{ ...BASE_ENV, OPENAI_BASE_URL: ai.baseUrl },
264+
);
265+
expect(result.code).not.toBe(0);
266+
expect(result.stdout + result.stderr).toMatch(/query|required param/i);
267+
}, 10000);
268+
});
269+
270+
// ─── npm (T-0017) ────────────────────────────────────────────────────────────
271+
272+
describe('ibr tool npm (T-0017)', () => {
273+
let ai, web;
274+
275+
beforeAll(async () => {
276+
web = await startStaticServer();
277+
ai = await startFromCassette('tool-npm', { SERVER_URL: web.baseUrl });
278+
}, 15000);
279+
280+
afterAll(async () => {
281+
await ai.close();
282+
await web.close();
283+
});
284+
285+
it('exits 0 and completes extraction without error', async () => {
286+
const result = await runIbr(
287+
['tool', 'npm', '--param', 'package=react'],
288+
{ ...BASE_ENV, OPENAI_BASE_URL: ai.baseUrl },
289+
);
290+
expect(result.code).toBe(0);
291+
expect(result.stdout + result.stderr).toMatch(/Task execution completed/i);
292+
}, 30000);
293+
294+
it('package is required — missing → non-zero exit before browser', async () => {
295+
const result = await runIbr(
296+
['tool', 'npm'],
297+
{ ...BASE_ENV, OPENAI_BASE_URL: ai.baseUrl },
298+
);
299+
expect(result.code).not.toBe(0);
300+
expect(result.stdout + result.stderr).toMatch(/package|required param/i);
301+
}, 10000);
302+
});
303+
304+
// ─── reddit (T-0020) ─────────────────────────────────────────────────────────
305+
306+
describe('ibr tool reddit (T-0020)', () => {
307+
let ai, web;
308+
309+
beforeAll(async () => {
310+
web = await startStaticServer();
311+
ai = await startFromCassette('tool-reddit', { SERVER_URL: web.baseUrl });
312+
}, 15000);
313+
314+
afterAll(async () => {
315+
await ai.close();
316+
await web.close();
317+
});
318+
319+
it('exits 0 and completes extraction without error', async () => {
320+
const result = await runIbr(
321+
['tool', 'reddit', '--param', 'query=rust async'],
322+
{ ...BASE_ENV, OPENAI_BASE_URL: ai.baseUrl },
323+
);
324+
expect(result.code).toBe(0);
325+
expect(result.stdout + result.stderr).toMatch(/Task execution completed/i);
326+
}, 30000);
327+
328+
it('query is required — missing → non-zero exit before browser', async () => {
329+
const result = await runIbr(
330+
['tool', 'reddit'],
331+
{ ...BASE_ENV, OPENAI_BASE_URL: ai.baseUrl },
332+
);
333+
expect(result.code).not.toBe(0);
334+
expect(result.stdout + result.stderr).toMatch(/query|required param/i);
335+
}, 10000);
336+
});
337+
338+
// ─── hackernews (T-0021) ─────────────────────────────────────────────────────
339+
340+
describe('ibr tool hackernews (T-0021)', () => {
341+
let ai, web;
342+
343+
beforeAll(async () => {
344+
web = await startStaticServer();
345+
ai = await startFromCassette('tool-hackernews', { SERVER_URL: web.baseUrl });
346+
}, 15000);
347+
348+
afterAll(async () => {
349+
await ai.close();
350+
await web.close();
351+
});
352+
353+
it('exits 0 and completes extraction without error', async () => {
354+
const result = await runIbr(
355+
['tool', 'hackernews', '--param', 'query=rust async'],
356+
{ ...BASE_ENV, OPENAI_BASE_URL: ai.baseUrl },
357+
);
358+
expect(result.code).toBe(0);
359+
expect(result.stdout + result.stderr).toMatch(/Task execution completed/i);
360+
}, 30000);
361+
362+
it('query is required — missing → non-zero exit before browser', async () => {
363+
const result = await runIbr(
364+
['tool', 'hackernews'],
365+
{ ...BASE_ENV, OPENAI_BASE_URL: ai.baseUrl },
366+
);
367+
expect(result.code).not.toBe(0);
368+
expect(result.stdout + result.stderr).toMatch(/query|required param/i);
369+
}, 10000);
370+
});
371+
372+
// ─── yahoo-finance (T-0022) ───────────────────────────────────────────────────
373+
374+
describe('ibr tool yahoo-finance (T-0022)', () => {
375+
let ai, web;
376+
377+
beforeAll(async () => {
378+
web = await startStaticServer();
379+
ai = await startFromCassette('tool-yahoo-finance', { SERVER_URL: web.baseUrl });
380+
}, 15000);
381+
382+
afterAll(async () => {
383+
await ai.close();
384+
await web.close();
385+
});
386+
387+
it('exits 0 and completes extraction without error', async () => {
388+
const result = await runIbr(
389+
['tool', 'yahoo-finance', '--param', 'ticker=AAPL'],
390+
{ ...BASE_ENV, OPENAI_BASE_URL: ai.baseUrl },
391+
);
392+
expect(result.code).toBe(0);
393+
expect(result.stdout + result.stderr).toMatch(/Task execution completed/i);
394+
}, 30000);
395+
396+
it('ticker is required — missing → non-zero exit before browser', async () => {
397+
const result = await runIbr(
398+
['tool', 'yahoo-finance'],
399+
{ ...BASE_ENV, OPENAI_BASE_URL: ai.baseUrl },
400+
);
401+
expect(result.code).not.toBe(0);
402+
expect(result.stdout + result.stderr).toMatch(/ticker|required param/i);
403+
}, 10000);
404+
});
405+
406+
// ─── dockerhub (T-0023) ──────────────────────────────────────────────────────
407+
408+
describe('ibr tool dockerhub (T-0023)', () => {
409+
let ai, web;
410+
411+
beforeAll(async () => {
412+
web = await startStaticServer();
413+
ai = await startFromCassette('tool-dockerhub', { SERVER_URL: web.baseUrl });
414+
}, 15000);
415+
416+
afterAll(async () => {
417+
await ai.close();
418+
await web.close();
419+
});
420+
421+
it('exits 0 and completes extraction without error', async () => {
422+
const result = await runIbr(
423+
['tool', 'dockerhub', '--param', 'image=nginx'],
424+
{ ...BASE_ENV, OPENAI_BASE_URL: ai.baseUrl },
425+
);
426+
expect(result.code).toBe(0);
427+
expect(result.stdout + result.stderr).toMatch(/Task execution completed/i);
428+
}, 30000);
429+
430+
it('image is required — missing → non-zero exit before browser', async () => {
431+
const result = await runIbr(
432+
['tool', 'dockerhub'],
433+
{ ...BASE_ENV, OPENAI_BASE_URL: ai.baseUrl },
434+
);
435+
expect(result.code).not.toBe(0);
436+
expect(result.stdout + result.stderr).toMatch(/image|required param/i);
437+
}, 10000);
438+
});
439+
440+
// ─── wikipedia (T-0014) ───────────────────────────────────────────────────────
441+
442+
describe('ibr tool wikipedia (T-0014)', () => {
443+
let ai, web;
444+
445+
beforeAll(async () => {
446+
web = await startStaticServer();
447+
ai = await startFromCassette('tool-wikipedia', { SERVER_URL: web.baseUrl });
448+
}, 15000);
449+
450+
afterAll(async () => {
451+
await ai.close();
452+
await web.close();
453+
});
454+
455+
it('exits 0 and completes extraction without error', async () => {
456+
const result = await runIbr(
457+
['tool', 'wikipedia', '--param', 'topic=Playwright'],
458+
{ ...BASE_ENV, OPENAI_BASE_URL: ai.baseUrl },
459+
);
460+
expect(result.code).toBe(0);
461+
expect(result.stdout + result.stderr).toMatch(/Task execution completed/i);
462+
}, 30000);
463+
464+
it('topic is required — missing → non-zero exit before browser', async () => {
465+
const result = await runIbr(
466+
['tool', 'wikipedia'],
467+
{ ...BASE_ENV, OPENAI_BASE_URL: ai.baseUrl },
468+
);
469+
expect(result.code).not.toBe(0);
470+
expect(result.stdout + result.stderr).toMatch(/topic|required param/i);
471+
}, 10000);
472+
});
473+
474+
// ─── amazon (T-0015) ─────────────────────────────────────────────────────────
475+
476+
describe('ibr tool amazon (T-0015)', () => {
477+
let ai, web;
478+
479+
beforeAll(async () => {
480+
web = await startStaticServer();
481+
ai = await startFromCassette('tool-amazon', { SERVER_URL: web.baseUrl });
482+
}, 15000);
483+
484+
afterAll(async () => {
485+
await ai.close();
486+
await web.close();
487+
});
488+
489+
it('exits 0 and completes extraction without error', async () => {
490+
const result = await runIbr(
491+
['tool', 'amazon', '--param', 'query=noise cancelling headphones'],
492+
{ ...BASE_ENV, OPENAI_BASE_URL: ai.baseUrl },
493+
);
494+
expect(result.code).toBe(0);
495+
expect(result.stdout + result.stderr).toMatch(/Task execution completed/i);
496+
}, 30000);
497+
498+
it('query is required — missing → non-zero exit before browser', async () => {
499+
const result = await runIbr(
500+
['tool', 'amazon'],
501+
{ ...BASE_ENV, OPENAI_BASE_URL: ai.baseUrl },
502+
);
503+
expect(result.code).not.toBe(0);
504+
expect(result.stdout + result.stderr).toMatch(/query|required param/i);
505+
}, 10000);
506+
});

tools/ebay.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: ebay
2+
description: "Search eBay listings; optionally filter to sold prices"
3+
params:
4+
- name: query
5+
description: "Search terms (e.g. 'iphone 15 pro 256gb')"
6+
required: true
7+
- name: max_results
8+
description: "Maximum number of listings to extract"
9+
default: "5"
10+
- name: sold
11+
description: "Show only sold/completed listings (true/false)"
12+
default: "false"
13+
- name: country
14+
description: "eBay country TLD (com, co.uk, de, fr, ca, com.au, etc.)"
15+
default: "com"
16+
url: "https://www.ebay.{{country}}/sch/i.html?_nkw={{query}}"
17+
instructions:
18+
- navigate to https://www.ebay.{{country}}/sch/i.html?_nkw={{query}}{% if sold == "true" %}&LH_Sold=1&LH_Complete=1{% endif %}
19+
- wait for the search results to load
20+
- extract up to {{max_results}} listings; for each include:
21+
- title: full listing title
22+
- price: listed price (or sold price if sold=true)
23+
- condition: item condition (New, Used, Refurbished, etc.)
24+
- sold: true if the item has been sold/completed, false otherwise
25+
- url: full URL of the listing

tools/npm.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: npm
2+
description: "Fetch npm package info: version, downloads, license, dependents"
3+
params:
4+
- name: package
5+
description: "npm package name (e.g. react, lodash, express)"
6+
required: true
7+
url: "https://www.npmjs.com/package/{{package}}"
8+
instructions:
9+
- navigate to https://www.npmjs.com/package/{{package}}
10+
- wait for the page to load fully
11+
- extract the following fields:
12+
- name: package name
13+
- version: latest published version
14+
- description: package description
15+
- weekly_downloads: weekly download count (numeric string)
16+
- license: license identifier (e.g. MIT, Apache-2.0)
17+
- homepage: homepage URL if present
18+
- github: GitHub repository URL if linked
19+
- dependents: number of packages that depend on this package

0 commit comments

Comments
 (0)