shop-client 3.14.1 → 3.16.0

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
@@ -409,6 +409,24 @@ const showcasedProducts = await shop.products.showcased();
409
409
 
410
410
  **Returns:** `Product[]` - Array of featured products
411
411
 
412
+ #### `products.infoHtml(productHandle, content?)`
413
+
414
+ Fetches the extracted HTML content from the product page. This is useful for getting the main product description and content directly from the page HTML.
415
+
416
+ ```typescript
417
+ // Fetch from store
418
+ const html = await shop.products.infoHtml("product-handle");
419
+
420
+ // Use provided HTML
421
+ const htmlFromContent = await shop.products.infoHtml("product-handle", "<html>...</html>");
422
+ ```
423
+
424
+ **Parameters:**
425
+ - `productHandle` (string): The product handle
426
+ - `content` (string, optional): HTML content to extract from. If provided, skips fetching the product page.
427
+
428
+ **Returns:** `Promise<string | null>` - Extracted HTML content or null if not found
429
+
412
430
  #### `products.filter()`
413
431
 
414
432
  Creates a map of variant options and their distinct values from all products in the store. This is useful for building filter interfaces, search facets, and product option selectors.
@@ -699,14 +717,11 @@ Determine the store’s primary verticals and target audiences using showcased p
699
717
  ```typescript
700
718
  import { ShopClient } from 'shop-client';
701
719
 
702
- const shop = new ShopClient('your-store-domain.com');
720
+ const shop = new ShopClient('your-store-domain.com', {
721
+ openRouter: { apiKey: 'YOUR_OPENROUTER_API_KEY', model: 'openai/gpt-4o-mini' },
722
+ });
703
723
 
704
724
  const breakdown = await shop.determineStoreType({
705
- // Optional: provide an OpenRouter API key for online classification
706
- // Offline mode falls back to regex heuristics if no key is set
707
- apiKey: process.env.OPENROUTER_API_KEY,
708
- // Optional: model name when using online classification
709
- model: 'openai/gpt-4o-mini',
710
725
  // Optional: limit the number of showcased products sampled (default 10, max 50)
711
726
  maxShowcaseProducts: 12,
712
727
  // Note: showcased collections are not used for classification
@@ -724,34 +739,45 @@ Details:
724
739
  - Uses only `product.bodyHtml` for classification (no images or external text).
725
740
  - Samples up to `maxShowcaseProducts` from `getInfo().showcase.products`.
726
741
  - Aggregates per-product audience/vertical into a multi-audience breakdown.
727
- - If `OPENROUTER_API_KEY` is absent or `OPENROUTER_OFFLINE=1`, uses offline regex heuristics.
742
+ - If `openRouter.offline` is `true`, uses offline regex heuristics. Otherwise, an OpenRouter API key is required (via `ShopClient` options or `determineStoreType({ apiKey })`).
728
743
  - Applies store-level pruning based on title/description to improve consistency.
729
744
 
730
745
  ### AI Enrichment
731
746
 
732
747
  ```typescript
733
- import { classifyProduct, generateSEOContent } from 'shop-client';
748
+ import { ShopClient } from 'shop-client';
749
+
750
+ const shop = new ShopClient('your-store-domain.com', {
751
+ openRouter: { apiKey: 'YOUR_OPENROUTER_API_KEY', model: 'openai/gpt-4o-mini' },
752
+ });
753
+
754
+ // Merge API (Ajax) + product page content into a clean description
755
+ const enrichedMarkdown = await shop.products.enriched('some-product-handle', {
756
+ outputFormat: 'markdown',
757
+ });
758
+ // enrichedMarkdown?.enriched_content → markdown
759
+
760
+ // Use provided content instead of fetching product page
761
+ const enrichedFromContent = await shop.products.enriched('some-product-handle', {
762
+ outputFormat: 'markdown',
763
+ content: '<html>...</html>',
764
+ });
734
765
 
735
- // Classify a single product (offline or via OpenRouter when configured)
736
- const classification = await classifyProduct({
737
- title: 'Organic Cotton T-Shirt',
738
- bodyHtml: '<p>Soft, breathable cotton tee</p>',
739
- tags: ['organic', 'cotton', 'unisex'],
766
+ const enrichedJson = await shop.products.enriched('some-product-handle', {
767
+ outputFormat: 'json',
740
768
  });
741
- // classification{ adult_unisex: { clothing: ['t-shirts'] } }
769
+ // enrichedJson?.enriched_contentJSON string (validated)
742
770
 
743
- // Generate basic SEO content for a product
744
- const seo = await generateSEOContent({
745
- title: 'Organic Cotton T-Shirt',
746
- description: 'Soft, breathable tee for everyday wear',
747
- tags: ['organic', 'cotton', 'unisex'],
771
+ // Build prompts without calling the LLM (useful for debugging)
772
+ const { system, user } = await shop.products.enrichedPrompts('some-product-handle', {
773
+ outputFormat: 'markdown',
774
+ content: '<html>...</html>', // Optional content
748
775
  });
749
- // seo.metaTitle, seo.metaDescription, seo.keywords
750
776
  ```
751
777
 
752
778
  Notes:
753
- - `classifyProduct` mirrors the store-level classification logic but operates on a single product.
754
- - `generateSEOContent` produces lightweight, deterministic metadata suitable for catalogs and PDPs.
779
+ - `enriched()` returns `enriched_content` as either markdown or a validated JSON string (based on `outputFormat`).
780
+ - `enrichedPrompts()` and `classifyPrompts()` return the prompt pair without making network calls.
755
781
 
756
782
  ## 🏗️ Type Definitions
757
783
 
@@ -1,5 +1,23 @@
1
- import { k as ProductClassification, l as SEOContent, a as ShopifySingleProduct } from '../types-luPg5O08.js';
1
+ import { O as OpenRouterConfig, k as ProductClassification, l as SEOContent, m as SystemUserPrompt, a as ShopifySingleProduct } from '../types-BRXamZMS.js';
2
2
 
3
+ declare function buildEnrichPrompt(args: {
4
+ bodyInput: string;
5
+ pageInput: string;
6
+ inputType: "markdown" | "html";
7
+ outputFormat: "markdown" | "json";
8
+ }): SystemUserPrompt;
9
+ declare function buildClassifyPrompt(productContent: string): SystemUserPrompt;
10
+ declare function buildEnrichPromptForProduct(domain: string, handle: string, options?: {
11
+ useGfm?: boolean;
12
+ inputType?: "markdown" | "html";
13
+ outputFormat?: "markdown" | "json";
14
+ htmlContent?: string;
15
+ }): Promise<SystemUserPrompt>;
16
+ declare function buildClassifyPromptForProduct(domain: string, handle: string, options?: {
17
+ useGfm?: boolean;
18
+ inputType?: "markdown" | "html";
19
+ htmlContent?: string;
20
+ }): Promise<SystemUserPrompt>;
3
21
  interface EnrichedProductResult {
4
22
  bodyHtml: string;
5
23
  pageHtml: string;
@@ -26,7 +44,7 @@ declare function extractMainSection(html: string): string | null;
26
44
  */
27
45
  declare function htmlToMarkdown(html: string | null, options?: {
28
46
  useGfm?: boolean;
29
- }): string;
47
+ }): Promise<string>;
30
48
  /**
31
49
  * Merge the two markdown sources using OpenAI GPT
32
50
  */
@@ -35,6 +53,7 @@ declare function mergeWithLLM(bodyInput: string, pageInput: string, options?: {
35
53
  inputType?: "markdown" | "html";
36
54
  model?: string;
37
55
  outputFormat?: "markdown" | "json";
56
+ openRouter?: OpenRouterConfig;
38
57
  }): Promise<string>;
39
58
  /**
40
59
  * MAIN WORKFLOW
@@ -45,6 +64,8 @@ declare function enrichProduct(domain: string, handle: string, options?: {
45
64
  inputType?: "markdown" | "html";
46
65
  model?: string;
47
66
  outputFormat?: "markdown" | "json";
67
+ openRouter?: OpenRouterConfig;
68
+ htmlContent?: string;
48
69
  }): Promise<EnrichedProductResult>;
49
70
  /**
50
71
  * Classify product content into a three-tier hierarchy using LLM.
@@ -53,6 +74,7 @@ declare function enrichProduct(domain: string, handle: string, options?: {
53
74
  declare function classifyProduct(productContent: string, options?: {
54
75
  apiKey?: string;
55
76
  model?: string;
77
+ openRouter?: OpenRouterConfig;
56
78
  }): Promise<ProductClassification>;
57
79
  /**
58
80
  * Generate SEO and marketing content for a product. Returns strictly validated JSON.
@@ -66,6 +88,7 @@ declare function generateSEOContent(product: {
66
88
  }, options?: {
67
89
  apiKey?: string;
68
90
  model?: string;
91
+ openRouter?: OpenRouterConfig;
69
92
  }): Promise<SEOContent>;
70
93
  /**
71
94
  * Determine store type (primary vertical and audience) from store information.
@@ -87,7 +110,8 @@ declare function determineStoreType(storeInfo: {
87
110
  }, options?: {
88
111
  apiKey?: string;
89
112
  model?: string;
113
+ openRouter?: OpenRouterConfig;
90
114
  }): Promise<Partial<Record<ProductClassification["audience"], Partial<Record<ProductClassification["vertical"], string[]>>>>>;
91
115
  declare function pruneBreakdownForSignals(breakdown: Partial<Record<ProductClassification["audience"], Partial<Record<ProductClassification["vertical"], string[]>>>>, text: string): Partial<Record<ProductClassification["audience"], Partial<Record<ProductClassification["vertical"], string[]>>>>;
92
116
 
93
- export { type EnrichedProductResult, classifyProduct, determineStoreType, enrichProduct, extractMainSection, fetchAjaxProduct, fetchProductPage, generateSEOContent, htmlToMarkdown, mergeWithLLM, pruneBreakdownForSignals };
117
+ export { type EnrichedProductResult, buildClassifyPrompt, buildClassifyPromptForProduct, buildEnrichPrompt, buildEnrichPromptForProduct, classifyProduct, determineStoreType, enrichProduct, extractMainSection, fetchAjaxProduct, fetchProductPage, generateSEOContent, htmlToMarkdown, mergeWithLLM, pruneBreakdownForSignals };
@@ -1,4 +1,8 @@
1
1
  import {
2
+ buildClassifyPrompt,
3
+ buildClassifyPromptForProduct,
4
+ buildEnrichPrompt,
5
+ buildEnrichPromptForProduct,
2
6
  classifyProduct,
3
7
  determineStoreType,
4
8
  enrichProduct,
@@ -9,9 +13,13 @@ import {
9
13
  htmlToMarkdown,
10
14
  mergeWithLLM,
11
15
  pruneBreakdownForSignals
12
- } from "../chunk-GNIBTUEK.mjs";
16
+ } from "../chunk-ZX4IG4TY.mjs";
13
17
  import "../chunk-D5MTUWFO.mjs";
14
18
  export {
19
+ buildClassifyPrompt,
20
+ buildClassifyPromptForProduct,
21
+ buildEnrichPrompt,
22
+ buildEnrichPromptForProduct,
15
23
  classifyProduct,
16
24
  determineStoreType,
17
25
  enrichProduct,
@@ -171,6 +171,19 @@ async function getInfoForShop(args, options) {
171
171
  (handle) => Boolean(handle)
172
172
  );
173
173
  }
174
+ const dedupeByNormalized = (arr) => {
175
+ var _a2;
176
+ const out = [];
177
+ const seen = /* @__PURE__ */ new Set();
178
+ for (const h of arr) {
179
+ const base = (_a2 = h == null ? void 0 : h.split("?")[0]) == null ? void 0 : _a2.replace(/^\/|\/$/g, "");
180
+ if (!base) continue;
181
+ if (seen.has(base)) continue;
182
+ seen.add(base);
183
+ out.push(base);
184
+ }
185
+ return out;
186
+ };
174
187
  const info = {
175
188
  name: name || slug,
176
189
  domain: sanitizeDomain(baseUrl),
@@ -182,8 +195,8 @@ async function getInfoForShop(args, options) {
182
195
  contactLinks,
183
196
  headerLinks,
184
197
  showcase: {
185
- products: unique(homePageProductLinks != null ? homePageProductLinks : []),
186
- collections: unique(homePageCollectionLinks != null ? homePageCollectionLinks : [])
198
+ products: dedupeByNormalized(homePageProductLinks != null ? homePageProductLinks : []),
199
+ collections: dedupeByNormalized(homePageCollectionLinks != null ? homePageCollectionLinks : [])
187
200
  },
188
201
  jsonLdData: ((_p = (_o = html.match(
189
202
  /<script[^>]*type="application\/ld\+json"[^>]*>([^<]+)<\/script>/g
@@ -230,8 +230,19 @@ function createCollectionOperations(baseUrl, storeDomain, fetchCollections, coll
230
230
  */
231
231
  showcased: async () => {
232
232
  const storeInfo = await getStoreInfo();
233
+ const normalizedHandles = storeInfo.showcase.collections.map((h) => {
234
+ var _a;
235
+ return (_a = h.split("?")[0]) == null ? void 0 : _a.replace(/^\/|\/$/g, "");
236
+ }).filter((base) => Boolean(base));
237
+ const seen = /* @__PURE__ */ new Set();
238
+ const uniqueHandles = [];
239
+ for (const base of normalizedHandles) {
240
+ if (seen.has(base)) continue;
241
+ seen.add(base);
242
+ uniqueHandles.push(base);
243
+ }
233
244
  const collections = await Promise.all(
234
- storeInfo.showcase.collections.map(
245
+ uniqueHandles.map(
235
246
  (collectionHandle) => findCollection(collectionHandle)
236
247
  )
237
248
  );
@@ -7,7 +7,7 @@ import {
7
7
 
8
8
  // src/products.ts
9
9
  import { filter, isNonNullish } from "remeda";
10
- function createProductOperations(baseUrl, storeDomain, fetchProducts, productsDto, productDto, getStoreInfo, findProduct) {
10
+ function createProductOperations(baseUrl, storeDomain, fetchProducts, productsDto, productDto, getStoreInfo, findProduct, ai) {
11
11
  const cacheExpiryMs = 5 * 60 * 1e3;
12
12
  const findCache = /* @__PURE__ */ new Map();
13
13
  const getCached = (key) => {
@@ -242,12 +242,6 @@ function createProductOperations(baseUrl, storeDomain, fetchProducts, productsDt
242
242
  if (!productHandle || typeof productHandle !== "string") {
243
243
  throw new Error("Product handle is required and must be a string");
244
244
  }
245
- const apiKey = (options == null ? void 0 : options.apiKey) || process.env.OPENROUTER_API_KEY;
246
- if (!apiKey) {
247
- throw new Error(
248
- "Missing OpenRouter API key. Pass options.apiKey or set OPENROUTER_API_KEY."
249
- );
250
- }
251
245
  const baseProduct = await operations.find(productHandle);
252
246
  if (!baseProduct) {
253
247
  return null;
@@ -255,32 +249,46 @@ function createProductOperations(baseUrl, storeDomain, fetchProducts, productsDt
255
249
  const handle = baseProduct.handle;
256
250
  const { enrichProduct } = await import("./ai/enrich.mjs");
257
251
  const enriched = await enrichProduct(storeDomain, handle, {
258
- apiKey,
252
+ apiKey: options == null ? void 0 : options.apiKey,
253
+ openRouter: ai == null ? void 0 : ai.openRouter,
259
254
  useGfm: options == null ? void 0 : options.useGfm,
260
255
  inputType: options == null ? void 0 : options.inputType,
261
256
  model: options == null ? void 0 : options.model,
262
- outputFormat: options == null ? void 0 : options.outputFormat
257
+ outputFormat: options == null ? void 0 : options.outputFormat,
258
+ htmlContent: options == null ? void 0 : options.content
263
259
  });
264
260
  return {
265
261
  ...baseProduct,
266
262
  enriched_content: enriched.mergedMarkdown
267
263
  };
268
264
  },
269
- classify: async (productHandle, options) => {
265
+ enrichedPrompts: async (productHandle, options) => {
270
266
  if (!productHandle || typeof productHandle !== "string") {
271
267
  throw new Error("Product handle is required and must be a string");
272
268
  }
273
- const apiKey = (options == null ? void 0 : options.apiKey) || process.env.OPENROUTER_API_KEY;
274
- if (!apiKey) {
275
- throw new Error(
276
- "Missing OpenRouter API key. Pass options.apiKey or set OPENROUTER_API_KEY."
277
- );
269
+ const baseProduct = await operations.find(productHandle);
270
+ if (!baseProduct) {
271
+ throw new Error("Product not found");
272
+ }
273
+ const handle = baseProduct.handle;
274
+ const { buildEnrichPromptForProduct } = await import("./ai/enrich.mjs");
275
+ return buildEnrichPromptForProduct(storeDomain, handle, {
276
+ useGfm: options == null ? void 0 : options.useGfm,
277
+ inputType: options == null ? void 0 : options.inputType,
278
+ outputFormat: options == null ? void 0 : options.outputFormat,
279
+ htmlContent: options == null ? void 0 : options.content
280
+ });
281
+ },
282
+ classify: async (productHandle, options) => {
283
+ if (!productHandle || typeof productHandle !== "string") {
284
+ throw new Error("Product handle is required and must be a string");
278
285
  }
279
286
  const enrichedProduct = await operations.enriched(productHandle, {
280
- apiKey,
287
+ apiKey: options == null ? void 0 : options.apiKey,
281
288
  inputType: "html",
282
289
  model: options == null ? void 0 : options.model,
283
- outputFormat: "json"
290
+ outputFormat: "json",
291
+ content: options == null ? void 0 : options.content
284
292
  });
285
293
  if (!enrichedProduct || !enrichedProduct.enriched_content) return null;
286
294
  let productContent = enrichedProduct.enriched_content;
@@ -304,20 +312,31 @@ function createProductOperations(baseUrl, storeDomain, fetchProducts, productsDt
304
312
  }
305
313
  const { classifyProduct } = await import("./ai/enrich.mjs");
306
314
  const classification = await classifyProduct(productContent, {
307
- apiKey,
315
+ apiKey: options == null ? void 0 : options.apiKey,
316
+ openRouter: ai == null ? void 0 : ai.openRouter,
308
317
  model: options == null ? void 0 : options.model
309
318
  });
310
319
  return classification;
311
320
  },
312
- generateSEOContent: async (productHandle, options) => {
321
+ classifyPrompts: async (productHandle, options) => {
313
322
  if (!productHandle || typeof productHandle !== "string") {
314
323
  throw new Error("Product handle is required and must be a string");
315
324
  }
316
- const apiKey = (options == null ? void 0 : options.apiKey) || process.env.OPENROUTER_API_KEY;
317
- if (!apiKey) {
318
- throw new Error(
319
- "Missing OpenRouter API key. Pass options.apiKey or set OPENROUTER_API_KEY."
320
- );
325
+ const baseProduct = await operations.find(productHandle);
326
+ if (!baseProduct) {
327
+ throw new Error("Product not found");
328
+ }
329
+ const handle = baseProduct.handle;
330
+ const { buildClassifyPromptForProduct } = await import("./ai/enrich.mjs");
331
+ return buildClassifyPromptForProduct(storeDomain, handle, {
332
+ useGfm: options == null ? void 0 : options.useGfm,
333
+ inputType: options == null ? void 0 : options.inputType,
334
+ htmlContent: options == null ? void 0 : options.content
335
+ });
336
+ },
337
+ generateSEOContent: async (productHandle, options) => {
338
+ if (!productHandle || typeof productHandle !== "string") {
339
+ throw new Error("Product handle is required and must be a string");
321
340
  }
322
341
  const baseProduct = await operations.find(productHandle);
323
342
  if (!baseProduct) return null;
@@ -328,13 +347,50 @@ function createProductOperations(baseUrl, storeDomain, fetchProducts, productsDt
328
347
  price: baseProduct.price,
329
348
  tags: baseProduct.tags
330
349
  };
331
- const { generateSEOContent: generateSEOContentLLM } = await import("./ai/enrich.mjs");
350
+ const {
351
+ extractMainSection,
352
+ fetchAjaxProduct,
353
+ fetchProductPage,
354
+ generateSEOContent: generateSEOContentLLM,
355
+ mergeWithLLM
356
+ } = await import("./ai/enrich.mjs");
332
357
  const seo = await generateSEOContentLLM(payload, {
333
- apiKey,
358
+ apiKey: options == null ? void 0 : options.apiKey,
359
+ openRouter: ai == null ? void 0 : ai.openRouter,
334
360
  model: options == null ? void 0 : options.model
335
361
  });
336
362
  return seo;
337
363
  },
364
+ /**
365
+ * Fetches the extracted HTML content from the product page.
366
+ * This is useful for getting the main product description and content directly from the page HTML.
367
+ *
368
+ * @param productHandle - The handle of the product
369
+ * @param content - Optional HTML content to extract from. If provided, skips fetching the product page.
370
+ * @returns {Promise<string | null>} The extracted HTML content or null if not found
371
+ *
372
+ * @example
373
+ * ```typescript
374
+ * // Fetch from store
375
+ * const html = await shop.products.infoHtml("product-handle");
376
+ *
377
+ * // Use provided HTML
378
+ * const htmlFromContent = await shop.products.infoHtml("product-handle", "<html>...</html>");
379
+ * ```
380
+ */
381
+ infoHtml: async (productHandle, content) => {
382
+ if (!productHandle || typeof productHandle !== "string") {
383
+ throw new Error("Product handle is required and must be a string");
384
+ }
385
+ const { extractMainSection, fetchProductPage } = await import("./ai/enrich.mjs");
386
+ if (content) {
387
+ return extractMainSection(content);
388
+ }
389
+ const baseProduct = await operations.find(productHandle);
390
+ if (!baseProduct) return null;
391
+ const pageHtml = await fetchProductPage(storeDomain, baseProduct.handle);
392
+ return extractMainSection(pageHtml);
393
+ },
338
394
  /**
339
395
  * Fetches products that are showcased/featured on the store's homepage.
340
396
  *
@@ -355,10 +411,19 @@ function createProductOperations(baseUrl, storeDomain, fetchProducts, productsDt
355
411
  */
356
412
  showcased: async () => {
357
413
  const storeInfo = await getStoreInfo();
414
+ const normalizedHandles = storeInfo.showcase.products.map((h) => {
415
+ var _a;
416
+ return (_a = h.split("?")[0]) == null ? void 0 : _a.replace(/^\/|\/$/g, "");
417
+ }).filter((base) => Boolean(base));
418
+ const seen = /* @__PURE__ */ new Set();
419
+ const uniqueHandles = [];
420
+ for (const base of normalizedHandles) {
421
+ if (seen.has(base)) continue;
422
+ seen.add(base);
423
+ uniqueHandles.push(base);
424
+ }
358
425
  const products = await Promise.all(
359
- storeInfo.showcase.products.map(
360
- (productHandle) => findProduct(productHandle)
361
- )
426
+ uniqueHandles.map((productHandle) => findProduct(productHandle))
362
427
  );
363
428
  return filter(products, isNonNullish);
364
429
  },