basketeer 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,7 +23,7 @@ Run your weekly shop from code, the terminal, or an AI agent. Everything but sig
23
23
  <br>
24
24
 
25
25
  [![CI](https://github.com/tobyandrews1985/basketeer/actions/workflows/ci.yml/badge.svg)](https://github.com/tobyandrews1985/basketeer/actions/workflows/ci.yml)
26
- [![tests](https://img.shields.io/badge/tests-71%20passing-brightgreen.svg)](tests/)
26
+ [![tests](https://img.shields.io/badge/tests-75%20passing-brightgreen.svg)](tests/)
27
27
  [![license: MIT](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)
28
28
  [![node >=18](https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg)](https://nodejs.org)
29
29
 
@@ -39,7 +39,7 @@ Tesco has no public API, and the usual approach (scraping the DOM) shatters on t
39
39
  - **Agent-ready.** A stdio MCP server lets Claude or any MCP client run the shop. Read-only and destructive tools are annotated, and checkout never pays.
40
40
  - **Typed and lean.** One fully-typed client you import; the CLI and MCP server are built on it. The data path imports no third-party packages (the three runtime deps — commander, the MCP SDK, and zod — are pulled only by the CLI and MCP server). It is pure `fetch` with no Node-only APIs, so it runs on Node and Node-compatible runtimes.
41
41
  - **Safe.** `checkout()` stops at the payment URL. A human finishes 3-D Secure in a browser, by design.
42
- - **Tested.** 71 tests across the data plane and its parsers.
42
+ - **Tested.** 75 tests across the data plane and its parsers.
43
43
 
44
44
  ## Nutrition, the part nothing else has
45
45
 
@@ -141,6 +141,7 @@ The full grocery lifecycle, typed end to end:
141
141
 
142
142
  - **Nutrition** — typed macros and structured micros, normalized from a product's on-pack rows when present; filter and rank a search by nutrition (anonymous)
143
143
  - **Catalogue** — `search`, `getProduct`, `browseCategory` (anonymous); `favourites` / "my usuals" (authed)
144
+ - **Product images** — `imageUrl` on every product/result; `resizeImageUrl(url, { width, height })` for thumbnails (anonymous)
144
145
  - **Basket** — `add`, `set`, `remove`, `get`
145
146
  - **Slots** — delivery and collection: `list` / `book` / `release`
146
147
  - **Orders** — `list`, `amend`, `cancel`, `lastFulfilled` (reorder)
@@ -260,7 +261,7 @@ Personal-account interoperability automation: your account, your data. The clien
260
261
 
261
262
  ```bash
262
263
  npm install
263
- npm test # 71 tests: vitest unit + regression + smoke
264
+ npm test # 75 tests: vitest unit + regression + smoke
264
265
  npm run build # clean build to dist/
265
266
  npm run example:lookup
266
267
  ```
package/dist/cli.js CHANGED
@@ -8,10 +8,10 @@
8
8
  * to stderr with a non-zero exit. Per the SDK's ethics ceiling, `checkout` only
9
9
  * hands back a payment URL — a human completes payment in a browser.
10
10
  */
11
- import { fileURLToPath } from "node:url";
12
11
  import { Command, InvalidArgumentError } from "commander";
13
12
  import { BrowserAuthBackend } from "./auth/browser/playwright.js";
14
13
  import { Basketeer, BasketeerError, FileTokenStore } from "./index.js";
14
+ import { isMainModule } from "./is-main.js";
15
15
  /** Pretty-print any JSON-serialisable value to stdout. */
16
16
  function emit(value) {
17
17
  process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
@@ -201,7 +201,7 @@ program
201
201
  });
202
202
  // --- entrypoint -------------------------------------------------------------
203
203
  // Only parse argv when run as a binary, so the module is importable (tests).
204
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
204
+ if (isMainModule(import.meta.url)) {
205
205
  program.parseAsync().catch((err) => {
206
206
  const name = err instanceof BasketeerError ? err.name : "Error";
207
207
  const message = err instanceof Error ? err.message : String(err);
@@ -0,0 +1,5 @@
1
+ export interface ImageSize {
2
+ width: number;
3
+ height: number;
4
+ }
5
+ export declare function resizeImageUrl(imageUrl: string | null, size: ImageSize): string | null;
package/dist/images.js ADDED
@@ -0,0 +1,15 @@
1
+ function assertPositiveInteger(value, name) {
2
+ if (!Number.isInteger(value) || value < 1) {
3
+ throw new RangeError(`${name} must be a positive integer`);
4
+ }
5
+ }
6
+ export function resizeImageUrl(imageUrl, size) {
7
+ if (imageUrl === null)
8
+ return null;
9
+ assertPositiveInteger(size.width, "width");
10
+ assertPositiveInteger(size.height, "height");
11
+ const url = new URL(imageUrl);
12
+ url.searchParams.set("w", String(size.width));
13
+ url.searchParams.set("h", String(size.height));
14
+ return url.toString();
15
+ }
package/dist/index.d.ts CHANGED
@@ -3,6 +3,8 @@ export type { AuthBackend, Credentials, OtpProvider } from "./auth/types.js";
3
3
  export type { Amendment, BasketeerOptions, BasketItemInput } from "./client.js";
4
4
  export { Basketeer } from "./client.js";
5
5
  export { ApiKeyError, AuthExpiredError, BasketeerError, GraphQLRequestError, LineRejectedError, NotFoundError, RateLimitedError, } from "./errors.js";
6
+ export type { ImageSize } from "./images.js";
7
+ export { resizeImageUrl } from "./images.js";
6
8
  export type { Basket, BasketLine, BookedSlot, MacroFilterKey, Macros, Micronutrient, Nutrition, NutritionBasis, NutritionFilter, NutritionSort, Order, OrderAddress, OrderItem, OrderSlot, PackSize, Price, Product, Promotion, Range, SearchPage, SearchResult, Session, Slot, } from "./models.js";
7
9
  export { filterByNutrition, parseNutrition } from "./nutrition.js";
8
10
  export type { FulfilmentType, OrderContext } from "./operations.js";
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ export { keepCookie, sessionFromCookies } from "./auth/harvest.js";
4
4
  export { Basketeer } from "./client.js";
5
5
  // Errors
6
6
  export { ApiKeyError, AuthExpiredError, BasketeerError, GraphQLRequestError, LineRejectedError, NotFoundError, RateLimitedError, } from "./errors.js";
7
+ export { resizeImageUrl } from "./images.js";
7
8
  // Nutrition
8
9
  export { filterByNutrition, parseNutrition } from "./nutrition.js";
9
10
  // Config & helpers
@@ -0,0 +1,8 @@
1
+ /**
2
+ * True when `moduleUrl` (pass `import.meta.url`) is the entrypoint the process
3
+ * was launched with. Resolves symlinks first, so it still holds when the module
4
+ * runs via an installed bin — npx, a global install, or `node_modules/.bin` —
5
+ * where `process.argv[1]` is a symlink to the real file and a raw `===` against
6
+ * `fileURLToPath(import.meta.url)` would wrongly be false.
7
+ */
8
+ export declare function isMainModule(moduleUrl: string): boolean;
@@ -0,0 +1,20 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+ /**
4
+ * True when `moduleUrl` (pass `import.meta.url`) is the entrypoint the process
5
+ * was launched with. Resolves symlinks first, so it still holds when the module
6
+ * runs via an installed bin — npx, a global install, or `node_modules/.bin` —
7
+ * where `process.argv[1]` is a symlink to the real file and a raw `===` against
8
+ * `fileURLToPath(import.meta.url)` would wrongly be false.
9
+ */
10
+ export function isMainModule(moduleUrl) {
11
+ const entry = process.argv[1];
12
+ if (!entry)
13
+ return false;
14
+ try {
15
+ return realpathSync(entry) === fileURLToPath(moduleUrl);
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
@@ -22,12 +22,12 @@
22
22
  * Run: `basketeer-mcp` (stdio). Configure it as an MCP server in your agent.
23
23
  */
24
24
  import { createHash } from "node:crypto";
25
- import { fileURLToPath } from "node:url";
26
25
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
27
26
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
28
27
  import { z } from "zod";
29
28
  import { BrowserAuthBackend } from "./auth/browser/playwright.js";
30
29
  import { Basketeer, BasketeerError, FileTokenStore } from "./index.js";
30
+ import { isMainModule } from "./is-main.js";
31
31
  /** Wrap a value as MCP JSON text content. */
32
32
  function json(value) {
33
33
  return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
@@ -55,7 +55,7 @@ export function confirmToken(parts) {
55
55
  * Pure: no filesystem/auth/transport side effects — safe to call from a test.
56
56
  */
57
57
  export function buildServer(client) {
58
- const server = new McpServer({ name: "basketeer", version: "0.1.0" });
58
+ const server = new McpServer({ name: "basketeer", version: "0.1.1" });
59
59
  // --- reads (anonymous OK) -------------------------------------------------
60
60
  server.tool("basketeer_search", "Search the Tesco grocery catalogue. Returns matching products with SKU, title, price, and any offers.", {
61
61
  query: z.string().describe("Search terms, e.g. 'semi skimmed milk'."),
@@ -153,7 +153,7 @@ export function buildServer(client) {
153
153
  // Only build the client (which reads ~/.basketeer/session.json) and attach the
154
154
  // stdio transport when run as a binary, so the module can be imported (e.g. by a
155
155
  // smoke test) without any filesystem/auth side effects or starting the server.
156
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
156
+ if (isMainModule(import.meta.url)) {
157
157
  const store = new FileTokenStore();
158
158
  const client = new Basketeer({
159
159
  session: await store.load(),
package/dist/models.d.ts CHANGED
@@ -22,6 +22,7 @@ export interface Product {
22
22
  tpnb: string | null;
23
23
  title: string;
24
24
  brand: string | null;
25
+ imageUrl: string | null;
25
26
  price: Price;
26
27
  packSize: PackSize | null;
27
28
  promotions: Promotion[];
@@ -36,6 +37,7 @@ export interface SearchResult {
36
37
  tpnb: string | null;
37
38
  title: string;
38
39
  brand: string | null;
40
+ imageUrl: string | null;
39
41
  price: Price;
40
42
  onOffer: boolean;
41
43
  promotions: Promotion[];
package/dist/parsers.js CHANGED
@@ -43,6 +43,7 @@ export function parseProduct(v) {
43
43
  tpnb: id(node.tpnb),
44
44
  title: str(node.title) ?? "",
45
45
  brand: str(node.brandName),
46
+ imageUrl: str(node.defaultImageUrl),
46
47
  price: parsePrice(node.price),
47
48
  packSize: parsePackSize(details.packSize),
48
49
  promotions: parsePromotions(node.promotions),
@@ -67,6 +68,7 @@ export function parseProductNode(v) {
67
68
  tpnb: id(node.tpnb),
68
69
  title: str(node.title) ?? "",
69
70
  brand: str(node.brandName),
71
+ imageUrl: str(node.defaultImageUrl),
70
72
  price: parsePrice(seller.price),
71
73
  onOffer: promotions.length > 0,
72
74
  promotions,
package/dist/queries.js CHANGED
@@ -5,7 +5,7 @@
5
5
  export const GET_PRODUCT = `
6
6
  query GetProduct($tpnc: String!) {
7
7
  product(tpnc: $tpnc) {
8
- tpnb tpnc title brandName
8
+ tpnb tpnc title brandName defaultImageUrl
9
9
  price { actual unitPrice unitOfMeasure }
10
10
  promotions {
11
11
  description
@@ -28,7 +28,7 @@ query Search($query: String!, $page: Int = 1, $count: Int) {
28
28
  node {
29
29
  __typename
30
30
  ... on ProductInterface {
31
- tpnc tpnb title brandName
31
+ tpnc tpnb title brandName defaultImageUrl
32
32
  sellers {
33
33
  results {
34
34
  price { actual unitPrice unitOfMeasure }
@@ -53,7 +53,7 @@ query GetCategoryProducts($facet: ID, $page: Int = 1, $count: Int) {
53
53
  node {
54
54
  __typename
55
55
  ... on ProductInterface {
56
- tpnc tpnb title brandName
56
+ tpnc tpnb title brandName defaultImageUrl
57
57
  sellers {
58
58
  results {
59
59
  price { actual unitPrice unitOfMeasure }
@@ -71,7 +71,7 @@ query GetFavourites($page: Int = 1, $count: Int, $sortBy: String) {
71
71
  products {
72
72
  __typename
73
73
  ... on ProductInterface {
74
- tpnc tpnb title brandName
74
+ tpnc tpnb title brandName defaultImageUrl
75
75
  sellers {
76
76
  results {
77
77
  price { actual unitPrice unitOfMeasure }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "basketeer",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Typed, pure-HTTP TypeScript SDK for your own Tesco grocery account — search, basket, delivery slots, and orders over plain fetch. Browser only for login + payment.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -56,12 +56,16 @@
56
56
  "keywords": [
57
57
  "tesco",
58
58
  "groceries",
59
+ "grocery",
60
+ "supermarket",
61
+ "nutrition",
59
62
  "automation",
60
63
  "graphql",
61
64
  "client",
62
65
  "mcp",
63
66
  "cli",
64
- "ai-agent"
67
+ "ai-agent",
68
+ "uk"
65
69
  ],
66
70
  "dependencies": {
67
71
  "@modelcontextprotocol/sdk": "^1.0.0",