@jackwener/opencli 1.5.7 → 1.5.9

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.
Files changed (199) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +17 -1
  3. package/README.zh-CN.md +17 -1
  4. package/dist/browser/base-page.d.ts +48 -0
  5. package/dist/browser/base-page.js +160 -0
  6. package/dist/browser/cdp.js +4 -106
  7. package/dist/browser/daemon-client.d.ts +1 -7
  8. package/dist/browser/daemon-client.js +2 -9
  9. package/dist/browser/discover.d.ts +1 -4
  10. package/dist/browser/discover.js +1 -4
  11. package/dist/browser/errors.d.ts +4 -0
  12. package/dist/browser/errors.js +20 -0
  13. package/dist/browser/index.d.ts +1 -1
  14. package/dist/browser/index.js +1 -1
  15. package/dist/browser/page.d.ts +6 -35
  16. package/dist/browser/page.js +10 -189
  17. package/dist/browser/tabs.js +5 -5
  18. package/dist/browser.test.js +15 -15
  19. package/dist/cli-manifest.json +294 -22
  20. package/dist/clis/amazon/bestsellers.d.ts +21 -0
  21. package/dist/clis/amazon/bestsellers.js +130 -0
  22. package/dist/clis/amazon/bestsellers.test.js +20 -0
  23. package/dist/clis/amazon/discussion.d.ts +20 -0
  24. package/dist/clis/amazon/discussion.js +91 -0
  25. package/dist/clis/amazon/discussion.test.js +36 -0
  26. package/dist/clis/amazon/offer.d.ts +23 -0
  27. package/dist/clis/amazon/offer.js +140 -0
  28. package/dist/clis/amazon/offer.test.d.ts +1 -0
  29. package/dist/clis/amazon/offer.test.js +29 -0
  30. package/dist/clis/amazon/product.d.ts +18 -0
  31. package/dist/clis/amazon/product.js +92 -0
  32. package/dist/clis/amazon/product.test.d.ts +1 -0
  33. package/dist/clis/amazon/product.test.js +24 -0
  34. package/dist/clis/amazon/search.d.ts +18 -0
  35. package/dist/clis/amazon/search.js +87 -0
  36. package/dist/clis/amazon/search.test.d.ts +1 -0
  37. package/dist/clis/amazon/search.test.js +22 -0
  38. package/dist/clis/amazon/shared.d.ts +64 -0
  39. package/dist/clis/amazon/shared.js +255 -0
  40. package/dist/clis/amazon/shared.test.d.ts +1 -0
  41. package/dist/clis/amazon/shared.test.js +33 -0
  42. package/dist/clis/gemini/ask.d.ts +1 -0
  43. package/dist/clis/gemini/ask.js +40 -0
  44. package/dist/clis/gemini/image.d.ts +1 -0
  45. package/dist/clis/gemini/image.js +105 -0
  46. package/dist/clis/gemini/new.d.ts +1 -0
  47. package/dist/clis/gemini/new.js +20 -0
  48. package/dist/clis/gemini/utils.d.ts +34 -0
  49. package/dist/clis/gemini/utils.js +463 -0
  50. package/dist/clis/gemini/utils.test.d.ts +1 -0
  51. package/dist/clis/gemini/utils.test.js +31 -0
  52. package/dist/clis/notebooklm/compat.test.d.ts +1 -1
  53. package/dist/clis/notebooklm/compat.test.js +3 -3
  54. package/dist/clis/notebooklm/current.js +2 -3
  55. package/dist/clis/notebooklm/get.js +2 -3
  56. package/dist/clis/notebooklm/history.js +2 -3
  57. package/dist/clis/notebooklm/note-list.js +2 -3
  58. package/dist/clis/notebooklm/notes-get.js +2 -3
  59. package/dist/clis/notebooklm/open.d.ts +1 -0
  60. package/dist/clis/notebooklm/open.js +41 -0
  61. package/dist/clis/notebooklm/open.test.d.ts +1 -0
  62. package/dist/clis/notebooklm/open.test.js +63 -0
  63. package/dist/clis/notebooklm/source-fulltext.js +2 -3
  64. package/dist/clis/notebooklm/source-get.js +2 -3
  65. package/dist/clis/notebooklm/source-guide.js +2 -3
  66. package/dist/clis/notebooklm/source-list.js +2 -3
  67. package/dist/clis/notebooklm/status.js +1 -2
  68. package/dist/clis/notebooklm/summary.js +2 -3
  69. package/dist/clis/notebooklm/utils.d.ts +2 -1
  70. package/dist/clis/notebooklm/utils.js +20 -21
  71. package/dist/clis/xiaohongshu/creator-note-detail.test.js +11 -11
  72. package/dist/clis/xiaohongshu/creator-notes-summary.test.js +6 -6
  73. package/dist/clis/xiaohongshu/creator-notes.test.js +22 -22
  74. package/dist/commanderAdapter.js +6 -3
  75. package/dist/commanderAdapter.test.js +33 -0
  76. package/dist/commands/daemon.js +1 -1
  77. package/dist/commands/daemon.test.js +1 -1
  78. package/dist/doctor.d.ts +1 -2
  79. package/dist/doctor.js +7 -8
  80. package/dist/explore.js +1 -1
  81. package/dist/extension-manifest-regression.test.js +1 -0
  82. package/dist/output.js +28 -0
  83. package/dist/output.test.js +15 -0
  84. package/dist/pipeline/executor.js +2 -7
  85. package/dist/pipeline/steps/browser.js +1 -1
  86. package/dist/pipeline/template.js +25 -3
  87. package/dist/record.d.ts +50 -0
  88. package/dist/record.js +298 -57
  89. package/dist/record.test.d.ts +1 -0
  90. package/dist/record.test.js +293 -0
  91. package/dist/registry.d.ts +2 -0
  92. package/dist/registry.js +1 -0
  93. package/dist/registry.test.js +10 -0
  94. package/dist/runtime.js +3 -3
  95. package/dist/snapshotFormatter.d.ts +1 -1
  96. package/dist/snapshotFormatter.js +4 -4
  97. package/dist/snapshotFormatter.test.d.ts +1 -1
  98. package/dist/snapshotFormatter.test.js +2 -2
  99. package/dist/types.d.ts +3 -1
  100. package/dist/types.js +1 -1
  101. package/docs/.vitepress/config.mts +2 -0
  102. package/docs/adapters/browser/amazon.md +53 -0
  103. package/docs/adapters/browser/gemini.md +72 -0
  104. package/docs/adapters/browser/notebooklm.md +5 -5
  105. package/docs/adapters/index.md +3 -1
  106. package/extension/dist/background.js +614 -794
  107. package/extension/manifest.json +2 -1
  108. package/extension/src/background.test.ts +7 -163
  109. package/extension/src/background.ts +7 -156
  110. package/extension/src/cdp.test.ts +75 -0
  111. package/extension/src/cdp.ts +77 -3
  112. package/extension/src/protocol.ts +1 -5
  113. package/package.json +1 -1
  114. package/skills/opencli-explorer/SKILL.md +847 -0
  115. package/skills/opencli-oneshot/SKILL.md +216 -0
  116. package/skills/opencli-usage/SKILL.md +71 -0
  117. package/skills/opencli-usage/browser.md +429 -0
  118. package/skills/opencli-usage/desktop.md +118 -0
  119. package/skills/opencli-usage/plugins.md +82 -0
  120. package/skills/opencli-usage/public-api.md +149 -0
  121. package/src/browser/base-page.ts +197 -0
  122. package/src/browser/cdp.ts +7 -131
  123. package/src/browser/daemon-client.ts +3 -14
  124. package/src/browser/discover.ts +1 -4
  125. package/src/browser/errors.ts +22 -0
  126. package/src/browser/index.ts +1 -1
  127. package/src/browser/page.ts +13 -212
  128. package/src/browser/tabs.ts +5 -5
  129. package/src/browser.test.ts +15 -15
  130. package/src/clis/amazon/bestsellers.test.ts +22 -0
  131. package/src/clis/amazon/bestsellers.ts +180 -0
  132. package/src/clis/amazon/discussion.test.ts +38 -0
  133. package/src/clis/amazon/discussion.ts +131 -0
  134. package/src/clis/amazon/offer.test.ts +35 -0
  135. package/src/clis/amazon/offer.ts +185 -0
  136. package/src/clis/amazon/product.test.ts +26 -0
  137. package/src/clis/amazon/product.ts +131 -0
  138. package/src/clis/amazon/search.test.ts +24 -0
  139. package/src/clis/amazon/search.ts +128 -0
  140. package/src/clis/amazon/shared.test.ts +37 -0
  141. package/src/clis/amazon/shared.ts +316 -0
  142. package/src/clis/gemini/ask.ts +46 -0
  143. package/src/clis/gemini/image.ts +115 -0
  144. package/src/clis/gemini/new.ts +22 -0
  145. package/src/clis/gemini/utils.test.ts +36 -0
  146. package/src/clis/gemini/utils.ts +523 -0
  147. package/src/clis/notebooklm/compat.test.ts +3 -3
  148. package/src/clis/notebooklm/current.ts +2 -3
  149. package/src/clis/notebooklm/get.ts +1 -3
  150. package/src/clis/notebooklm/history.ts +1 -3
  151. package/src/clis/notebooklm/note-list.ts +1 -3
  152. package/src/clis/notebooklm/notes-get.ts +1 -3
  153. package/src/clis/notebooklm/open.test.ts +78 -0
  154. package/src/clis/notebooklm/open.ts +61 -0
  155. package/src/clis/notebooklm/source-fulltext.ts +1 -3
  156. package/src/clis/notebooklm/source-get.ts +1 -3
  157. package/src/clis/notebooklm/source-guide.ts +1 -3
  158. package/src/clis/notebooklm/source-list.ts +1 -3
  159. package/src/clis/notebooklm/status.ts +1 -2
  160. package/src/clis/notebooklm/summary.ts +1 -3
  161. package/src/clis/notebooklm/utils.ts +29 -20
  162. package/src/clis/xiaohongshu/creator-note-detail.test.ts +11 -11
  163. package/src/clis/xiaohongshu/creator-notes-summary.test.ts +6 -6
  164. package/src/clis/xiaohongshu/creator-notes.test.ts +22 -22
  165. package/src/commanderAdapter.test.ts +47 -0
  166. package/src/commanderAdapter.ts +7 -3
  167. package/src/commands/daemon.test.ts +1 -1
  168. package/src/commands/daemon.ts +1 -1
  169. package/src/doctor.ts +7 -8
  170. package/src/explore.ts +1 -1
  171. package/src/extension-manifest-regression.test.ts +1 -0
  172. package/src/output.test.ts +17 -0
  173. package/src/output.ts +27 -0
  174. package/src/pipeline/executor.ts +2 -7
  175. package/src/pipeline/steps/browser.ts +1 -1
  176. package/src/pipeline/template.ts +27 -4
  177. package/src/record.test.ts +362 -0
  178. package/src/record.ts +341 -62
  179. package/src/registry.test.ts +12 -0
  180. package/src/registry.ts +3 -0
  181. package/src/runtime.ts +3 -3
  182. package/src/snapshotFormatter.test.ts +2 -2
  183. package/src/snapshotFormatter.ts +4 -4
  184. package/src/types.ts +3 -1
  185. package/.agents/skills/cross-project-adapter-migration/SKILL.md +0 -249
  186. package/.agents/workflows/cross-project-adapter-migration.md +0 -54
  187. package/SKILL.md +0 -879
  188. package/dist/clis/notebooklm/bind-current.js +0 -29
  189. package/dist/clis/notebooklm/bind-current.test.d.ts +0 -1
  190. package/dist/clis/notebooklm/bind-current.test.js +0 -35
  191. package/dist/clis/notebooklm/binding.test.js +0 -44
  192. package/src/clis/notebooklm/bind-current.test.ts +0 -43
  193. package/src/clis/notebooklm/bind-current.ts +0 -36
  194. package/src/clis/notebooklm/binding.test.ts +0 -53
  195. /package/dist/browser/{mcp.d.ts → bridge.d.ts} +0 -0
  196. /package/dist/browser/{mcp.js → bridge.js} +0 -0
  197. /package/dist/clis/{notebooklm/bind-current.d.ts → amazon/bestsellers.test.d.ts} +0 -0
  198. /package/dist/clis/{notebooklm/binding.test.d.ts → amazon/discussion.test.d.ts} +0 -0
  199. /package/src/browser/{mcp.ts → bridge.ts} +0 -0
@@ -0,0 +1,140 @@
1
+ import { CommandExecutionError } from '../../errors.js';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ import { buildProductUrl, buildProvenance, cleanText, extractAsin, isAmazonEntity, normalizeProductUrl, PRIMARY_PRICE_SELECTORS, parsePriceText, assertUsableState, gotoAndReadState, } from './shared.js';
4
+ const OFFER_FACT_SELECTOR = [
5
+ '#sellerProfileTriggerId',
6
+ '#shipsFromSoldByInsideBuyBox_feature_div',
7
+ '#fulfillerInfoFeature_feature_div',
8
+ '#merchantInfoFeature_feature_div',
9
+ '#tabular-buybox-container',
10
+ '#merchant-info',
11
+ ].join(', ');
12
+ function collapseAdjacentWords(text) {
13
+ const parts = cleanText(text).split(' ').filter(Boolean);
14
+ const deduped = [];
15
+ for (const part of parts) {
16
+ if (deduped[deduped.length - 1] === part)
17
+ continue;
18
+ deduped.push(part);
19
+ }
20
+ return deduped.join(' ');
21
+ }
22
+ function extractShipsFrom(text) {
23
+ const normalized = cleanText(text);
24
+ const match = normalized.match(/Ships from\s+(.+?)(?=Sold by|and Fulfilled by|$)/i);
25
+ return match ? collapseAdjacentWords(match[1].replace(/Ships from/ig, '')) : null;
26
+ }
27
+ function extractSoldBy(text) {
28
+ const normalized = cleanText(text);
29
+ const match = normalized.match(/Sold by\s+(.+?)(?=and Fulfilled by|Ships from|$)/i);
30
+ return match ? collapseAdjacentWords(match[1]) : null;
31
+ }
32
+ function isDeliveryLocationBlocked(text) {
33
+ const normalized = cleanText(text).toLowerCase();
34
+ return normalized.includes('cannot be shipped to your selected delivery location')
35
+ || normalized.includes('similar items shipping to')
36
+ || normalized.includes('deliver to hong kong');
37
+ }
38
+ function normalizeOfferPayload(payload) {
39
+ const asin = extractAsin(payload.href ?? '') ?? null;
40
+ const sourceUrl = cleanText(payload.href) || buildProductUrl(payload.href ?? '');
41
+ const price = parsePriceText(payload.price_text);
42
+ const merchantInfo = cleanText(payload.merchant_info) || null;
43
+ const soldBy = cleanText(payload.sold_by)
44
+ || extractSoldBy(payload.ships_from_text ?? '')
45
+ || extractSoldBy(merchantInfo ?? '')
46
+ || null;
47
+ const shipsFrom = extractShipsFrom(payload.ships_from_text ?? '')
48
+ || extractShipsFrom(merchantInfo ?? '')
49
+ || cleanText(payload.ships_from_text)
50
+ || null;
51
+ const provenance = buildProvenance(sourceUrl);
52
+ return {
53
+ asin,
54
+ product_url: normalizeProductUrl(payload.href),
55
+ ...provenance,
56
+ price_text: price.price_text,
57
+ price_value: price.price_value,
58
+ currency: price.currency,
59
+ merchant_info_text: merchantInfo,
60
+ sold_by: soldBy,
61
+ ships_from: shipsFrom,
62
+ offer_listing_url: cleanText(payload.offer_link) || null,
63
+ review_url: cleanText(payload.review_url) || null,
64
+ qa_url: cleanText(payload.qa_url) || null,
65
+ is_amazon_sold: isAmazonEntity(soldBy),
66
+ is_amazon_fulfilled: isAmazonEntity(shipsFrom) || /fulfilled by amazon/i.test(merchantInfo ?? ''),
67
+ };
68
+ }
69
+ async function readOfferPayload(page, input) {
70
+ const url = buildProductUrl(input);
71
+ const state = await gotoAndReadState(page, url, 2500, 'offer');
72
+ assertUsableState(state, 'offer');
73
+ // Reconnecting to an existing Amazon target can surface the product page
74
+ // before the buy-box / merchant blocks are reattached to the DOM.
75
+ await page.wait({ selector: OFFER_FACT_SELECTOR, timeout: 6 }).catch(() => { });
76
+ return await page.evaluate(`
77
+ (() => ({
78
+ href: window.location.href,
79
+ title: document.title || '',
80
+ price_text: (() => {
81
+ const selectors = ${JSON.stringify(PRIMARY_PRICE_SELECTORS)};
82
+ for (const selector of selectors) {
83
+ const text = document.querySelector(selector)?.textContent || '';
84
+ if (text.trim()) return text;
85
+ }
86
+ return '';
87
+ })(),
88
+ merchant_info: document.querySelector('#merchant-info')?.textContent || '',
89
+ sold_by: document.querySelector('#sellerProfileTriggerId')?.textContent || '',
90
+ ships_from_text:
91
+ document.querySelector('#shipsFromSoldByInsideBuyBox_feature_div')?.textContent
92
+ || document.querySelector('#fulfillerInfoFeature_feature_div')?.textContent
93
+ || document.querySelector('#merchantInfoFeature_feature_div')?.textContent
94
+ || document.querySelector('#tabular-buybox-container')?.textContent
95
+ || '',
96
+ offer_link: document.querySelector('a[href*="/gp/offer-listing/"]')?.href || '',
97
+ review_url: document.querySelector('a[href*="#customerReviews"]')?.href || '',
98
+ qa_url: document.querySelector('a[href*="ask/questions"]')?.href || '',
99
+ buybox_text:
100
+ document.querySelector('#desktop_qualifiedBuyBox')?.textContent
101
+ || document.querySelector('#buybox')?.textContent
102
+ || '',
103
+ }))()
104
+ `);
105
+ }
106
+ cli({
107
+ site: 'amazon',
108
+ name: 'offer',
109
+ description: 'Amazon seller, buy box, and fulfillment facts from the product page',
110
+ domain: 'amazon.com',
111
+ strategy: Strategy.COOKIE,
112
+ navigateBefore: false,
113
+ args: [
114
+ {
115
+ name: 'input',
116
+ required: true,
117
+ positional: true,
118
+ help: 'ASIN or product URL, for example B0FJS72893',
119
+ },
120
+ ],
121
+ columns: ['asin', 'price_text', 'sold_by', 'ships_from', 'is_amazon_sold', 'is_amazon_fulfilled'],
122
+ func: async (page, kwargs) => {
123
+ const input = String(kwargs.input ?? '');
124
+ const payload = await readOfferPayload(page, input);
125
+ const normalized = normalizeOfferPayload(payload);
126
+ if (!normalized.sold_by && !normalized.ships_from && !normalized.merchant_info_text) {
127
+ if (isDeliveryLocationBlocked(payload.buybox_text)) {
128
+ throw new CommandExecutionError('amazon offer buy box is blocked by the current delivery location', 'The shared Chrome profile is not set to the target US delivery address. Switch Amazon delivery location to the requested US destination, reopen the product page, and retry.');
129
+ }
130
+ throw new CommandExecutionError('amazon offer surface did not expose seller or fulfillment facts', 'The product page may have changed. Open the product page in Chrome, make sure the buy box is visible, and retry.');
131
+ }
132
+ return [normalized];
133
+ },
134
+ });
135
+ export const __test__ = {
136
+ extractShipsFrom,
137
+ extractSoldBy,
138
+ isDeliveryLocationBlocked,
139
+ normalizeOfferPayload,
140
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { __test__ } from './offer.js';
3
+ describe('amazon offer normalization', () => {
4
+ it('extracts sold-by and fulfillment facts from product offer text', () => {
5
+ const result = __test__.normalizeOfferPayload({
6
+ href: 'https://www.amazon.com/dp/B0FJS72893',
7
+ price_text: '$15.99',
8
+ merchant_info: '',
9
+ sold_by: 'KUATUDIRECT',
10
+ ships_from_text: 'Ships from Amazon',
11
+ offer_link: null,
12
+ review_url: 'https://www.amazon.com/dp/B0FJS72893#customerReviews',
13
+ qa_url: null,
14
+ });
15
+ expect(result.asin).toBe('B0FJS72893');
16
+ expect(result.sold_by).toBe('KUATUDIRECT');
17
+ expect(result.ships_from).toBe('Amazon');
18
+ expect(result.is_amazon_sold).toBe(false);
19
+ expect(result.is_amazon_fulfilled).toBe(true);
20
+ });
21
+ it('parses merchant info fallback text', () => {
22
+ expect(__test__.extractSoldBy('Sold by Example Seller and Fulfilled by Amazon.')).toBe('Example Seller');
23
+ expect(__test__.extractShipsFrom('Ships from Amazon')).toBe('Amazon');
24
+ });
25
+ it('detects delivery-location blocking in the buy box text', () => {
26
+ expect(__test__.isDeliveryLocationBlocked('This item cannot be shipped to your selected delivery location. Similar items shipping to Hong Kong')).toBe(true);
27
+ expect(__test__.isDeliveryLocationBlocked('Ships from Amazon')).toBe(false);
28
+ });
29
+ });
@@ -0,0 +1,18 @@
1
+ interface ProductPayload {
2
+ href?: string;
3
+ title?: string;
4
+ product_title?: string | null;
5
+ byline?: string | null;
6
+ price_text?: string | null;
7
+ rating_text?: string | null;
8
+ review_count_text?: string | null;
9
+ review_url?: string | null;
10
+ qa_url?: string | null;
11
+ bullets?: string[];
12
+ breadcrumbs?: string[];
13
+ }
14
+ declare function normalizeProductPayload(payload: ProductPayload): Record<string, unknown>;
15
+ export declare const __test__: {
16
+ normalizeProductPayload: typeof normalizeProductPayload;
17
+ };
18
+ export {};
@@ -0,0 +1,92 @@
1
+ import { CommandExecutionError } from '../../errors.js';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ import { buildProductUrl, buildProvenance, cleanText, extractAsin, PRIMARY_PRICE_SELECTORS, parsePriceText, parseRatingValue, parseReviewCount, normalizeProductUrl, uniqueNonEmpty, assertUsableState, gotoAndReadState, } from './shared.js';
4
+ const PRODUCT_TITLE_SELECTOR = '#productTitle, #title span, [data-feature-name="title"] h1 span';
5
+ const BYLINE_SELECTOR = '#bylineInfo, [data-feature-name="bylineInfo"] #bylineInfo';
6
+ function normalizeProductPayload(payload) {
7
+ const sourceUrl = cleanText(payload.href) || buildProductUrl(cleanText(payload.product_title) || cleanText(payload.href));
8
+ const asin = extractAsin(payload.href ?? '') ?? null;
9
+ const price = parsePriceText(payload.price_text);
10
+ const ratingText = cleanText(payload.rating_text) || null;
11
+ const reviewCountText = cleanText(payload.review_count_text) || null;
12
+ const provenance = buildProvenance(sourceUrl);
13
+ return {
14
+ asin,
15
+ title: cleanText(payload.product_title) || cleanText(payload.title) || null,
16
+ product_url: normalizeProductUrl(payload.href),
17
+ ...provenance,
18
+ brand_text: cleanText(payload.byline) || null,
19
+ price_text: price.price_text,
20
+ price_value: price.price_value,
21
+ currency: price.currency,
22
+ rating_text: ratingText,
23
+ rating_value: parseRatingValue(ratingText),
24
+ review_count_text: reviewCountText,
25
+ review_count: parseReviewCount(reviewCountText),
26
+ review_url: cleanText(payload.review_url) || null,
27
+ qa_url: cleanText(payload.qa_url) || null,
28
+ breadcrumbs: uniqueNonEmpty(payload.breadcrumbs ?? []),
29
+ bullet_points: uniqueNonEmpty(payload.bullets ?? []),
30
+ };
31
+ }
32
+ async function readProductPayload(page, input) {
33
+ const url = buildProductUrl(input);
34
+ const state = await gotoAndReadState(page, url, 2500, 'product');
35
+ assertUsableState(state, 'product');
36
+ // Amazon can report a "stable" DOM before the product title block hydrates,
37
+ // especially when reconnecting to an existing shared CDP target.
38
+ await page.wait({ selector: PRODUCT_TITLE_SELECTOR, timeout: 6 }).catch(() => { });
39
+ return await page.evaluate(`
40
+ (() => ({
41
+ href: window.location.href,
42
+ title: document.title || '',
43
+ product_title: document.querySelector(${JSON.stringify(PRODUCT_TITLE_SELECTOR)})?.textContent || '',
44
+ byline: document.querySelector(${JSON.stringify(BYLINE_SELECTOR)})?.textContent || '',
45
+ price_text: (() => {
46
+ const selectors = ${JSON.stringify(PRIMARY_PRICE_SELECTORS)};
47
+ for (const selector of selectors) {
48
+ const text = document.querySelector(selector)?.textContent || '';
49
+ if (text.trim()) return text;
50
+ }
51
+ return '';
52
+ })(),
53
+ rating_text:
54
+ document.querySelector('#acrPopover')?.getAttribute('title')
55
+ || document.querySelector('#acrPopover')?.textContent
56
+ || '',
57
+ review_count_text: document.querySelector('#acrCustomerReviewText')?.textContent || '',
58
+ review_url: document.querySelector('a[href*="#customerReviews"]')?.href || '',
59
+ qa_url: document.querySelector('a[href*="ask/questions"]')?.href || '',
60
+ bullets: Array.from(document.querySelectorAll('#feature-bullets li .a-list-item')).map((node) => node.textContent || ''),
61
+ breadcrumbs: Array.from(document.querySelectorAll('#wayfinding-breadcrumbs_feature_div a')).map((node) => node.textContent || ''),
62
+ }))()
63
+ `);
64
+ }
65
+ cli({
66
+ site: 'amazon',
67
+ name: 'product',
68
+ description: 'Amazon product page facts for candidate validation',
69
+ domain: 'amazon.com',
70
+ strategy: Strategy.COOKIE,
71
+ navigateBefore: false,
72
+ args: [
73
+ {
74
+ name: 'input',
75
+ required: true,
76
+ positional: true,
77
+ help: 'ASIN or product URL, for example B0FJS72893',
78
+ },
79
+ ],
80
+ columns: ['asin', 'title', 'price_text', 'rating_value', 'review_count'],
81
+ func: async (page, kwargs) => {
82
+ const input = String(kwargs.input ?? '');
83
+ const payload = await readProductPayload(page, input);
84
+ if (!cleanText(payload.product_title)) {
85
+ throw new CommandExecutionError('amazon product page did not expose product content', 'The product page may have changed or hit a robot check. Open the product page in Chrome and retry.');
86
+ }
87
+ return [normalizeProductPayload(payload)];
88
+ },
89
+ });
90
+ export const __test__ = {
91
+ normalizeProductPayload,
92
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { __test__ } from './product.js';
3
+ describe('amazon product normalization', () => {
4
+ it('normalizes product facts from the product page', () => {
5
+ const result = __test__.normalizeProductPayload({
6
+ href: 'https://www.amazon.com/dp/B0FJS72893',
7
+ title: 'Amazon.com: KVTUKIAIT Desktop Shelf Organizer',
8
+ product_title: 'White Desktop Shelf Organizer for Top of Desk',
9
+ byline: 'Visit the KVTUKIAIT Store',
10
+ price_text: '$15.99',
11
+ rating_text: '3.9 out of 5 stars',
12
+ review_count_text: '27 ratings',
13
+ review_url: 'https://www.amazon.com/dp/B0FJS72893#customerReviews',
14
+ qa_url: null,
15
+ bullets: ['SPACE-SAVING DESK SHELF ORGANIZER', 'SMALL AND STYLISH AESTHETIC DECOR'],
16
+ breadcrumbs: ['Office Products', 'Desktop & Off-Surface Shelves'],
17
+ });
18
+ expect(result.asin).toBe('B0FJS72893');
19
+ expect(result.price_value).toBe(15.99);
20
+ expect(result.rating_value).toBe(3.9);
21
+ expect(result.review_count).toBe(27);
22
+ expect(result.breadcrumbs).toEqual(['Office Products', 'Desktop & Off-Surface Shelves']);
23
+ });
24
+ });
@@ -0,0 +1,18 @@
1
+ interface SearchPayload {
2
+ href?: string;
3
+ cards?: Array<{
4
+ asin?: string;
5
+ title?: string;
6
+ href?: string;
7
+ price_text?: string | null;
8
+ rating_text?: string | null;
9
+ review_count_text?: string | null;
10
+ sponsored?: boolean;
11
+ badge_texts?: string[];
12
+ }>;
13
+ }
14
+ declare function normalizeSearchCandidate(candidate: NonNullable<SearchPayload['cards']>[number], rank: number, sourceUrl: string): Record<string, unknown>;
15
+ export declare const __test__: {
16
+ normalizeSearchCandidate: typeof normalizeSearchCandidate;
17
+ };
18
+ export {};
@@ -0,0 +1,87 @@
1
+ import { CommandExecutionError } from '../../errors.js';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ import { buildProvenance, buildSearchUrl, cleanText, extractAsin, normalizeProductUrl, parsePriceText, parseRatingValue, parseReviewCount, assertUsableState, gotoAndReadState, } from './shared.js';
4
+ function normalizeSearchCandidate(candidate, rank, sourceUrl) {
5
+ const productUrl = normalizeProductUrl(candidate.href);
6
+ const asin = extractAsin(candidate.asin ?? '') ?? extractAsin(productUrl ?? '') ?? null;
7
+ const price = parsePriceText(candidate.price_text);
8
+ const ratingText = cleanText(candidate.rating_text) || null;
9
+ const reviewCountText = cleanText(candidate.review_count_text) || null;
10
+ const provenance = buildProvenance(sourceUrl);
11
+ return {
12
+ rank,
13
+ asin,
14
+ title: cleanText(candidate.title) || null,
15
+ product_url: productUrl,
16
+ ...provenance,
17
+ price_text: price.price_text,
18
+ price_value: price.price_value,
19
+ currency: price.currency,
20
+ rating_text: ratingText,
21
+ rating_value: parseRatingValue(ratingText),
22
+ review_count_text: reviewCountText,
23
+ review_count: parseReviewCount(reviewCountText),
24
+ is_sponsored: candidate.sponsored === true,
25
+ badges: (candidate.badge_texts ?? []).map((value) => cleanText(value)).filter(Boolean),
26
+ };
27
+ }
28
+ async function readSearchPayload(page, query) {
29
+ const url = buildSearchUrl(query);
30
+ const state = await gotoAndReadState(page, url, 2500, 'search');
31
+ assertUsableState(state, 'search');
32
+ return await page.evaluate(`
33
+ (() => ({
34
+ href: window.location.href,
35
+ cards: Array.from(document.querySelectorAll('[data-component-type="s-search-result"]'))
36
+ .map((card) => ({
37
+ asin: card.getAttribute('data-asin') || '',
38
+ title: card.querySelector('h2')?.textContent || '',
39
+ href: card.querySelector('a.a-link-normal[href*="/dp/"]')?.href || '',
40
+ price_text: card.querySelector('.a-price .a-offscreen')?.textContent || '',
41
+ rating_text: card.querySelector('[aria-label*="out of 5 stars"]')?.getAttribute('aria-label') || '',
42
+ review_count_text: card.querySelector('a[href*="#customerReviews"]')?.textContent || '',
43
+ sponsored: /sponsored/i.test(card.innerText || ''),
44
+ badge_texts: Array.from(card.querySelectorAll('.a-badge-text')).map((node) => node.textContent || ''),
45
+ })),
46
+ }))()
47
+ `);
48
+ }
49
+ cli({
50
+ site: 'amazon',
51
+ name: 'search',
52
+ description: 'Amazon search results for product discovery and coarse filtering',
53
+ domain: 'amazon.com',
54
+ strategy: Strategy.COOKIE,
55
+ navigateBefore: false,
56
+ args: [
57
+ {
58
+ name: 'query',
59
+ required: true,
60
+ positional: true,
61
+ help: 'Search query, for example "desk shelf organizer"',
62
+ },
63
+ {
64
+ name: 'limit',
65
+ type: 'int',
66
+ default: 20,
67
+ help: 'Maximum number of results to return (default 20)',
68
+ },
69
+ ],
70
+ columns: ['rank', 'asin', 'title', 'price_text', 'rating_value', 'review_count'],
71
+ func: async (page, kwargs) => {
72
+ const query = String(kwargs.query ?? '');
73
+ const limit = Math.max(1, Number(kwargs.limit) || 20);
74
+ const payload = await readSearchPayload(page, query);
75
+ const sourceUrl = cleanText(payload.href) || buildSearchUrl(query);
76
+ const cards = (payload.cards ?? [])
77
+ .filter((card) => cleanText(card.asin) && cleanText(card.title))
78
+ .slice(0, limit);
79
+ if (cards.length === 0) {
80
+ throw new CommandExecutionError('amazon search did not expose any product cards', 'The search page may have changed or hit a robot check. Open the same query in Chrome, verify the page is visible, and retry.');
81
+ }
82
+ return cards.map((card, index) => normalizeSearchCandidate(card, index + 1, sourceUrl));
83
+ },
84
+ });
85
+ export const __test__ = {
86
+ normalizeSearchCandidate,
87
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { __test__ } from './search.js';
3
+ describe('amazon search normalization', () => {
4
+ it('normalizes search cards into research-friendly fields', () => {
5
+ const result = __test__.normalizeSearchCandidate({
6
+ asin: 'B0FJS72893',
7
+ title: 'White Desktop Shelf Organizer for Top of Desk',
8
+ href: 'https://www.amazon.com/KVTUKIAIT-White-Desktop-Shelf-Organizer/dp/B0FJS72893/ref=sr_1_1',
9
+ price_text: '$15.99',
10
+ rating_text: '3.9 out of 5 stars, rating details',
11
+ review_count_text: '(27)',
12
+ sponsored: false,
13
+ badge_texts: ['Limited time deal'],
14
+ }, 1, 'https://www.amazon.com/s?k=desk+shelf+organizer');
15
+ expect(result.asin).toBe('B0FJS72893');
16
+ expect(result.product_url).toBe('https://www.amazon.com/dp/B0FJS72893');
17
+ expect(result.price_value).toBe(15.99);
18
+ expect(result.rating_value).toBe(3.9);
19
+ expect(result.review_count).toBe(27);
20
+ expect(result.badges).toEqual(['Limited time deal']);
21
+ });
22
+ });
@@ -0,0 +1,64 @@
1
+ import type { IPage } from '../../types.js';
2
+ export declare const SITE = "amazon";
3
+ export declare const DOMAIN = "amazon.com";
4
+ export declare const HOME_URL = "https://www.amazon.com/";
5
+ export declare const BESTSELLERS_URL = "https://www.amazon.com/Best-Sellers/zgbs";
6
+ export declare const SEARCH_URL_PREFIX = "https://www.amazon.com/s?k=";
7
+ export declare const PRODUCT_URL_PREFIX = "https://www.amazon.com/dp/";
8
+ export declare const DISCUSSION_URL_PREFIX = "https://www.amazon.com/product-reviews/";
9
+ export declare const STRATEGY = "cookie";
10
+ export declare const PRIMARY_PRICE_SELECTORS: string[];
11
+ export interface ProvenanceFields {
12
+ source_url: string;
13
+ fetched_at: string;
14
+ strategy: string;
15
+ }
16
+ export interface PageState {
17
+ href: string;
18
+ title: string;
19
+ body_text: string;
20
+ }
21
+ export interface PriceValue {
22
+ price_text: string | null;
23
+ price_value: number | null;
24
+ currency: string | null;
25
+ }
26
+ export declare function cleanText(value: unknown): string;
27
+ export declare function cleanMultilineText(value: unknown): string;
28
+ export declare function uniqueNonEmpty(values: Array<string | null | undefined>): string[];
29
+ export declare function buildProvenance(sourceUrl: string): ProvenanceFields;
30
+ export declare function buildSearchUrl(query: string): string;
31
+ export declare function extractAsin(input: string): string | null;
32
+ export declare function buildProductUrl(input: string): string;
33
+ export declare function buildDiscussionUrl(input: string): string;
34
+ export declare function resolveBestsellersUrl(input?: string): string;
35
+ export declare function canonicalizeAmazonUrl(input: string): string;
36
+ export declare function toAbsoluteAmazonUrl(value: string | null | undefined): string | null;
37
+ export declare function normalizeProductUrl(value: string | null | undefined): string | null;
38
+ export declare function parsePriceText(text: string | null | undefined): PriceValue;
39
+ export declare function parseRatingValue(text: string | null | undefined): number | null;
40
+ export declare function parseReviewCount(text: string | null | undefined): number | null;
41
+ export declare function extractReviewCountFromCardText(text: string | null | undefined): string | null;
42
+ export declare function isAmazonEntity(text: string | null | undefined): boolean;
43
+ export declare function firstMeaningfulLine(text: string | null | undefined): string;
44
+ export declare function trimRatingPrefix(text: string | null | undefined): string | null;
45
+ export declare function isRobotState(state: Partial<PageState>): boolean;
46
+ export declare function buildChallengeHint(action: string): string;
47
+ export declare function readPageState(page: IPage): Promise<PageState>;
48
+ export declare function gotoAndReadState(page: IPage, url: string, settleMs?: number, action?: string): Promise<PageState>;
49
+ export declare function assertUsableState(state: PageState, action: string): void;
50
+ export declare const __test__: {
51
+ buildSearchUrl: typeof buildSearchUrl;
52
+ extractAsin: typeof extractAsin;
53
+ buildProductUrl: typeof buildProductUrl;
54
+ buildDiscussionUrl: typeof buildDiscussionUrl;
55
+ resolveBestsellersUrl: typeof resolveBestsellersUrl;
56
+ parsePriceText: typeof parsePriceText;
57
+ parseRatingValue: typeof parseRatingValue;
58
+ parseReviewCount: typeof parseReviewCount;
59
+ extractReviewCountFromCardText: typeof extractReviewCountFromCardText;
60
+ isAmazonEntity: typeof isAmazonEntity;
61
+ trimRatingPrefix: typeof trimRatingPrefix;
62
+ isRobotState: typeof isRobotState;
63
+ PRIMARY_PRICE_SELECTORS: string[];
64
+ };