@agentrhq/webcmd 0.2.1 → 0.2.3

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 (100) hide show
  1. package/README.md +39 -24
  2. package/cli-manifest.json +2308 -650
  3. package/clis/_shared/site-auth.js +6 -1
  4. package/clis/bigbasket/add-to-cart.js +82 -0
  5. package/clis/bigbasket/bigbasket.test.js +255 -0
  6. package/clis/bigbasket/cart.js +81 -0
  7. package/clis/bigbasket/category.js +30 -0
  8. package/clis/bigbasket/checkout.js +71 -0
  9. package/clis/bigbasket/location.js +30 -0
  10. package/clis/bigbasket/product.js +79 -0
  11. package/clis/bigbasket/search.js +30 -0
  12. package/clis/bigbasket/utils.js +207 -0
  13. package/clis/blinkit/add-to-cart.js +123 -0
  14. package/clis/blinkit/auth.js +99 -0
  15. package/clis/blinkit/blinkit.test.js +168 -0
  16. package/clis/blinkit/cart.js +34 -0
  17. package/clis/blinkit/checkout.js +32 -0
  18. package/clis/blinkit/location.js +29 -0
  19. package/clis/blinkit/place-order.js +78 -0
  20. package/clis/blinkit/product.js +63 -0
  21. package/clis/blinkit/search.js +89 -0
  22. package/clis/blinkit/utils.js +223 -0
  23. package/clis/district/_lib.js +566 -0
  24. package/clis/district/auth.js +49 -0
  25. package/clis/district/checkout.js +348 -0
  26. package/clis/district/listings.js +158 -0
  27. package/clis/district/locations.js +211 -0
  28. package/clis/district/search.js +218 -0
  29. package/clis/district/seats.js +233 -0
  30. package/clis/district/set-location.js +82 -0
  31. package/clis/district/showtimes.js +433 -0
  32. package/clis/practo/appointment.js +21 -0
  33. package/clis/practo/appointments.js +27 -0
  34. package/clis/practo/book-confirm.js +35 -0
  35. package/clis/practo/book-preview.js +24 -0
  36. package/clis/practo/booking-link.js +24 -0
  37. package/clis/practo/cancel.js +29 -0
  38. package/clis/practo/contact.js +21 -0
  39. package/clis/practo/login.js +21 -0
  40. package/clis/practo/practo.test.js +136 -0
  41. package/clis/practo/profile.js +31 -0
  42. package/clis/practo/search.js +30 -0
  43. package/clis/practo/slots.js +19 -0
  44. package/clis/practo/utils.js +374 -0
  45. package/clis/practo/whoami.js +28 -0
  46. package/clis/reddit/popular.js +12 -1
  47. package/clis/reddit/popular.test.js +12 -3
  48. package/clis/zepto/add-to-cart.js +53 -0
  49. package/clis/zepto/auth.js +59 -0
  50. package/clis/zepto/cart.js +23 -0
  51. package/clis/zepto/checkout.js +60 -0
  52. package/clis/zepto/location.js +20 -0
  53. package/clis/zepto/place-order.js +47 -0
  54. package/clis/zepto/product.js +52 -0
  55. package/clis/zepto/search.js +30 -0
  56. package/clis/zepto/utils.js +228 -0
  57. package/clis/zepto/zepto.test.js +238 -0
  58. package/dist/src/browser/bridge.d.ts +1 -0
  59. package/dist/src/browser/bridge.js +1 -1
  60. package/dist/src/browser/page.d.ts +4 -1
  61. package/dist/src/browser/page.js +12 -1
  62. package/dist/src/browser/protocol.d.ts +2 -0
  63. package/dist/src/browser/runtime/local-cloak/actions.js +1 -0
  64. package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +2 -0
  65. package/dist/src/browser/runtime/local-cloak/session-manager.js +12 -2
  66. package/dist/src/browser/runtime/local-cloak/session-manager.test.js +58 -0
  67. package/dist/src/build-manifest.js +1 -0
  68. package/dist/src/build-manifest.test.js +34 -0
  69. package/dist/src/cli.js +111 -28
  70. package/dist/src/discovery.js +1 -0
  71. package/dist/src/engine.test.js +62 -0
  72. package/dist/src/execution.js +1 -1
  73. package/dist/src/generate-release-notes-cli.test.js +55 -1
  74. package/dist/src/manifest-types.d.ts +2 -0
  75. package/dist/src/registry.d.ts +10 -0
  76. package/dist/src/registry.js +5 -3
  77. package/dist/src/registry.test.js +25 -0
  78. package/dist/src/release-notes.d.ts +3 -1
  79. package/dist/src/release-notes.js +44 -1
  80. package/dist/src/release-notes.test.js +39 -1
  81. package/dist/src/runtime.d.ts +2 -0
  82. package/dist/src/runtime.js +1 -0
  83. package/dist/src/skills.d.ts +23 -5
  84. package/dist/src/skills.js +87 -45
  85. package/dist/src/skills.test.js +80 -23
  86. package/package.json +2 -2
  87. package/scripts/generate-release-notes.ts +31 -0
  88. package/skills/smart-search/SKILL.md +156 -0
  89. package/skills/smart-search/references/sources-ai.md +74 -0
  90. package/skills/smart-search/references/sources-info.md +43 -0
  91. package/skills/smart-search/references/sources-media.md +40 -0
  92. package/skills/smart-search/references/sources-other.md +32 -0
  93. package/skills/smart-search/references/sources-shopping.md +21 -0
  94. package/skills/smart-search/references/sources-social.md +36 -0
  95. package/skills/smart-search/references/sources-tech.md +38 -0
  96. package/skills/smart-search/references/sources-travel.md +26 -0
  97. package/skills/webcmd-adapter-author/SKILL.md +10 -0
  98. package/skills/webcmd-adapter-author/references/adapter-template.md +2 -0
  99. package/skills/webcmd-autofix/SKILL.md +8 -0
  100. package/skills/webcmd-sitemap-author/SKILL.md +1 -1
@@ -46,6 +46,11 @@ export function registerSiteAuthCommands(config) {
46
46
  if (!config?.site || !config?.domain || !config?.loginUrl || typeof config.verify !== 'function') {
47
47
  throw new Error('registerSiteAuthCommands requires site, domain, loginUrl, and verify(page)');
48
48
  }
49
+ // Sites whose login is a modal/flow rather than a page can pass
50
+ // openLogin(page) to bring the login UI up; default is a plain navigation.
51
+ const openLogin = typeof config.openLogin === 'function'
52
+ ? config.openLogin
53
+ : async (page) => { await page.goto(config.loginUrl); };
49
54
 
50
55
  cli({
51
56
  site: config.site,
@@ -92,7 +97,7 @@ export function registerSiteAuthCommands(config) {
92
97
  if (!isAuthRequired(error)) throw error;
93
98
  }
94
99
 
95
- await page.goto(config.loginUrl);
100
+ await openLogin(page);
96
101
  const timeoutSeconds = Number(kwargs.timeout ?? DEFAULT_TIMEOUT_SECONDS);
97
102
  const deadline = Date.now() + timeoutSeconds * 1000;
98
103
  let lastAuthMessage = '';
@@ -0,0 +1,82 @@
1
+ import { CommandExecutionError } from '@agentrhq/webcmd/errors';
2
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
3
+ import { DOMAIN, parseQuantityArg, resolveProductInput, safeGoto, SITE } from './utils.js';
4
+
5
+ function addToCartEvaluate(productId, quantity) {
6
+ return `
7
+ (async () => {
8
+ const productId = ${JSON.stringify(productId)};
9
+ const quantity = ${Number(quantity) || 1};
10
+ const clean = (value) => value == null ? '' : String(value).replace(/\\s+/g, ' ').trim();
11
+ const text = clean(document.body?.innerText || '');
12
+ if (/select\\s+(?:size|weight|pack|option)|choose\\s+(?:size|weight|pack|option)/i.test(text)) {
13
+ return { ok: false, message: 'OPTION_REQUIRED' };
14
+ }
15
+
16
+ const quantityInput = document.querySelector('input[type="number"], input[aria-label*="quantity" i]');
17
+ if (quantityInput && quantity > 1) {
18
+ quantityInput.value = String(quantity);
19
+ quantityInput.dispatchEvent(new Event('input', { bubbles: true }));
20
+ quantityInput.dispatchEvent(new Event('change', { bubbles: true }));
21
+ }
22
+
23
+ const buttons = Array.from(document.querySelectorAll('button, [role="button"], a'));
24
+ const button = buttons.find((node) => {
25
+ const label = clean(node.textContent || node.getAttribute('aria-label') || node.getAttribute('title'));
26
+ return /^(add|add to basket|add to cart|basket)$/i.test(label) || /add\\s*(to)?\\s*(basket|cart)/i.test(label);
27
+ });
28
+ if (!button) return { ok: false, message: 'ADD_BUTTON_NOT_FOUND' };
29
+ button.click();
30
+ await new Promise((resolve) => setTimeout(resolve, 1500));
31
+
32
+ const afterText = clean(document.body?.innerText || '');
33
+ const ok = /added|basket|cart/i.test(afterText) && !/failed|error/i.test(afterText);
34
+ return {
35
+ ok,
36
+ message: ok ? 'SUCCESS' : 'UNCONFIRMED',
37
+ product_id: productId,
38
+ url: location.href,
39
+ };
40
+ })()
41
+ `;
42
+ }
43
+
44
+ cli({
45
+ site: SITE,
46
+ name: 'add-to-cart',
47
+ access: 'write',
48
+ description: 'Add a BigBasket product to cart',
49
+ domain: DOMAIN,
50
+ strategy: Strategy.COOKIE,
51
+ browser: true,
52
+ args: [
53
+ { name: 'product', required: true, positional: true, help: 'Product ID or URL' },
54
+ { name: 'quantity', type: 'int', default: 1, help: 'Quantity to add (max 20)' },
55
+ ],
56
+ columns: ['ok', 'product_id', 'quantity', 'url', 'message'],
57
+ func: async (page, kwargs) => {
58
+ const product = resolveProductInput(kwargs.product);
59
+ const quantity = parseQuantityArg(kwargs.quantity, 1, 20);
60
+ await safeGoto(page, product.url, 'bigbasket add-to-cart');
61
+ if (page.wait) await page.wait(2);
62
+ const result = await page.evaluate(addToCartEvaluate(product.productId, quantity)).catch((error) => {
63
+ throw new CommandExecutionError(`bigbasket add-to-cart evaluation failed: ${error?.message || error}`);
64
+ });
65
+ if (result?.message === 'OPTION_REQUIRED') {
66
+ throw new CommandExecutionError('This BigBasket product requires option selection and is not supported in v1.');
67
+ }
68
+ if (result?.message === 'ADD_BUTTON_NOT_FOUND') {
69
+ throw new CommandExecutionError('Could not find a BigBasket add-to-cart button.');
70
+ }
71
+ if (!result?.ok) {
72
+ throw new CommandExecutionError('Failed to confirm BigBasket add-to-cart success.');
73
+ }
74
+ return [{
75
+ ok: true,
76
+ product_id: product.productId,
77
+ quantity,
78
+ url: result.url || product.url,
79
+ message: 'Added to cart',
80
+ }];
81
+ },
82
+ });
@@ -0,0 +1,255 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { JSDOM } from 'jsdom';
3
+ import { ArgumentError, AuthRequiredError, CommandExecutionError } from '@agentrhq/webcmd/errors';
4
+ import { getRegistry } from '@agentrhq/webcmd/registry';
5
+ import './search.js';
6
+ import './category.js';
7
+ import './product.js';
8
+ import './add-to-cart.js';
9
+ import './cart.js';
10
+ import './checkout.js';
11
+ import './location.js';
12
+ import { CART_EVALUATE } from './cart.js';
13
+ import { CHECKOUT_REVIEW_EVALUATE } from './checkout.js';
14
+ import {
15
+ buildSearchUrl,
16
+ normalizeLocationState,
17
+ normalizeProductRow,
18
+ parseLimitArg,
19
+ parseQuantityArg,
20
+ productCardsEvaluate,
21
+ resolveCategoryUrl,
22
+ resolveProductInput,
23
+ } from './utils.js';
24
+
25
+ describe('bigbasket helpers', () => {
26
+ it('builds search and category URLs', () => {
27
+ expect(buildSearchUrl('amul milk')).toContain('/ps/?q=amul%20milk');
28
+ expect(resolveCategoryUrl('/pc/fruits-vegetables/vegetables/')).toBe('https://www.bigbasket.com/pc/fruits-vegetables/vegetables/');
29
+ expect(resolveCategoryUrl('fruits-vegetables/vegetables')).toBe('https://www.bigbasket.com/pc/fruits-vegetables/vegetables/');
30
+ });
31
+
32
+ it('parses product ids and URLs', () => {
33
+ expect(resolveProductInput('https://www.bigbasket.com/pd/40022638/fresho-banana-robusta-1-kg/')).toMatchObject({
34
+ productId: '40022638',
35
+ url: 'https://www.bigbasket.com/pd/40022638/fresho-banana-robusta-1-kg/',
36
+ });
37
+ expect(resolveProductInput('40022638')).toEqual({
38
+ productId: '40022638',
39
+ url: 'https://www.bigbasket.com/pd/40022638/',
40
+ });
41
+ });
42
+
43
+ it('fails fast on bad numbers and malformed inputs', () => {
44
+ expect(() => buildSearchUrl(' ')).toThrow(ArgumentError);
45
+ expect(() => resolveProductInput('banana')).toThrow(ArgumentError);
46
+ expect(() => parseLimitArg(0, 20, 50)).toThrow(ArgumentError);
47
+ expect(() => parseLimitArg(51, 20, 50)).toThrow(ArgumentError);
48
+ expect(() => parseQuantityArg(0, 1, 20)).toThrow(ArgumentError);
49
+ });
50
+
51
+ it('normalizes product rows', () => {
52
+ expect(normalizeProductRow({
53
+ product_id: '40022638',
54
+ title: 'Fresho Banana',
55
+ brand: 'Fresho',
56
+ pack_size: '1 kg',
57
+ price: '₹54',
58
+ mrp: '₹70',
59
+ discount: '23% OFF',
60
+ availability: 'In stock',
61
+ url: '/pd/40022638/fresho-banana/',
62
+ }, 0)).toEqual({
63
+ rank: 1,
64
+ product_id: '40022638',
65
+ title: 'Fresho Banana',
66
+ brand: 'Fresho',
67
+ pack_size: '1 kg',
68
+ price: 54,
69
+ mrp: 70,
70
+ discount: '23% OFF',
71
+ availability: 'In stock',
72
+ url: 'https://www.bigbasket.com/pd/40022638/fresho-banana/',
73
+ });
74
+ });
75
+
76
+ it('normalizes schema availability URLs', () => {
77
+ expect(normalizeProductRow({
78
+ product_id: '40090893',
79
+ title: 'Amul Gold Full Cream Milk',
80
+ availability: 'https://schema.org/InStock',
81
+ url: '/pd/40090893/amul-amul-gold/',
82
+ }, 0).availability).toBe('In stock');
83
+ });
84
+
85
+ it('normalizes selected location without leaking full address fields', () => {
86
+ expect(normalizeLocationState({
87
+ selectedAddressInfo: JSON.stringify({
88
+ nick: 'Home',
89
+ area: 'NTPC Township',
90
+ city_name: 'Noida',
91
+ pin: 201307,
92
+ address1: 'Block C-03 / 79',
93
+ address2: 'Samridhi',
94
+ landmark: 'Near something',
95
+ lat: 28.586165986798466,
96
+ lng: 77.35627826303244,
97
+ member: { full_name: 'Private Name' },
98
+ }),
99
+ selected_address_id: '218320015',
100
+ })).toEqual({
101
+ selected: true,
102
+ label: 'Home',
103
+ area: 'NTPC Township',
104
+ city: 'Noida',
105
+ pincode: '201307',
106
+ source: 'selectedAddressInfo',
107
+ });
108
+ });
109
+
110
+ it('extracts product title when image anchor appears before title anchor', () => {
111
+ const dom = new JSDOM(`
112
+ <ul>
113
+ <li>
114
+ <div><a href="/pd/40090893/amul-amul-gold-500-ml-pouch/"><img alt="milk"></a></div>
115
+ <h3>
116
+ <a href="/pd/40090893/amul-amul-gold-500-ml-pouch/">
117
+ <span>Amul</span>
118
+ <div><h3>Gold Full Cream Milk</h3></div>
119
+ </a>
120
+ </h3>
121
+ <span>500 ml - Pouch</span>
122
+ <span>₹34.00</span>
123
+ <button>Add</button>
124
+ </li>
125
+ </ul>
126
+ `, { runScripts: 'outside-only', url: 'https://www.bigbasket.com/ps/?q=milk' });
127
+
128
+ const result = dom.window.eval(productCardsEvaluate(3));
129
+
130
+ expect(result.rows[0]).toMatchObject({
131
+ product_id: '40090893',
132
+ brand: 'Amul',
133
+ title: 'Gold Full Cream Milk',
134
+ pack_size: '500 ml - Pouch',
135
+ price: '₹34.00',
136
+ });
137
+ });
138
+
139
+ it('uses listing URL title param when visible anchor text is only the brand', () => {
140
+ const dom = new JSDOM(`
141
+ <ul>
142
+ <li>
143
+ <a href="/pd/40090894/amul-taaza-500-ml-pouch/?t_s=Taaza+Milk"><span>Amul</span></a>
144
+ <span>500 ml</span>
145
+ <span>₹26.00</span>
146
+ <button>Add</button>
147
+ </li>
148
+ </ul>
149
+ `, { runScripts: 'outside-only', url: 'https://www.bigbasket.com/ps/?q=milk' });
150
+
151
+ const result = dom.window.eval(productCardsEvaluate(3));
152
+
153
+ expect(result.rows[0]).toMatchObject({
154
+ product_id: '40090894',
155
+ brand: 'Amul',
156
+ title: 'Taaza Milk',
157
+ });
158
+ });
159
+ });
160
+
161
+ describe('bigbasket registry shape', () => {
162
+ it('registers approved commands with expected access classes', () => {
163
+ expect(getRegistry().get('bigbasket/search').access).toBe('read');
164
+ expect(getRegistry().get('bigbasket/product').access).toBe('read');
165
+ expect(getRegistry().get('bigbasket/category').access).toBe('read');
166
+ expect(getRegistry().get('bigbasket/add-to-cart').access).toBe('write');
167
+ expect(getRegistry().get('bigbasket/cart').access).toBe('read');
168
+ expect(getRegistry().get('bigbasket/checkout').access).toBe('write');
169
+ expect(getRegistry().get('bigbasket/location').access).toBe('read');
170
+ });
171
+
172
+ it('keeps checkout in review mode', () => {
173
+ const checkout = getRegistry().get('bigbasket/checkout');
174
+ expect(checkout.columns).toEqual([
175
+ 'ok', 'stage', 'cart_total', 'address_ready', 'delivery_ready', 'payment_ready', 'next_action', 'url',
176
+ ]);
177
+ });
178
+ });
179
+
180
+ describe('bigbasket read commands', () => {
181
+ it('rejects invalid read arguments before navigation', async () => {
182
+ const fakePage = { goto: () => { throw new Error('should not navigate'); } };
183
+
184
+ await expect(getRegistry().get('bigbasket/search').func(fakePage, { query: ' ' })).rejects.toThrow(ArgumentError);
185
+ await expect(getRegistry().get('bigbasket/category').func(fakePage, { category: 'bad' })).rejects.toThrow(ArgumentError);
186
+ await expect(getRegistry().get('bigbasket/product').func(fakePage, { product: 'banana' })).rejects.toThrow(ArgumentError);
187
+ });
188
+
189
+ it('wraps read navigation failures as CommandExecutionError', async () => {
190
+ const fakePage = { goto: () => Promise.reject(new Error('browser down')) };
191
+
192
+ await expect(getRegistry().get('bigbasket/search').func(fakePage, { query: 'milk' })).rejects.toThrow(CommandExecutionError);
193
+ await expect(getRegistry().get('bigbasket/category').func(fakePage, { category: 'fruits-vegetables/vegetables' })).rejects.toThrow(CommandExecutionError);
194
+ await expect(getRegistry().get('bigbasket/product').func(fakePage, { product: '40022638' })).rejects.toThrow(CommandExecutionError);
195
+ });
196
+ });
197
+
198
+ describe('bigbasket cart and checkout commands', () => {
199
+ it('rejects invalid add-to-cart arguments before navigation', async () => {
200
+ const fakePage = { goto: () => { throw new Error('should not navigate'); } };
201
+
202
+ await expect(getRegistry().get('bigbasket/add-to-cart').func(fakePage, {})).rejects.toThrow(ArgumentError);
203
+ await expect(getRegistry().get('bigbasket/add-to-cart').func(fakePage, { product: 'banana' })).rejects.toThrow(ArgumentError);
204
+ await expect(getRegistry().get('bigbasket/add-to-cart').func(fakePage, { product: '40022638', quantity: 0 })).rejects.toThrow(ArgumentError);
205
+ });
206
+
207
+ it('wraps cart and add-to-cart navigation failures as CommandExecutionError', async () => {
208
+ const fakePage = { goto: () => Promise.reject(new Error('browser down')) };
209
+
210
+ await expect(getRegistry().get('bigbasket/add-to-cart').func(fakePage, { product: '40022638' })).rejects.toThrow(CommandExecutionError);
211
+ await expect(getRegistry().get('bigbasket/cart').func(fakePage, {})).rejects.toThrow(CommandExecutionError);
212
+ await expect(getRegistry().get('bigbasket/checkout').func(fakePage, {})).rejects.toThrow(CommandExecutionError);
213
+ });
214
+
215
+ it('reports auth-required state for cart and checkout instead of recommendation rows', async () => {
216
+ const fakePage = {
217
+ goto: () => Promise.resolve(),
218
+ wait: () => Promise.resolve(),
219
+ evaluate: () => Promise.resolve({ authRequired: true, rows: [] }),
220
+ };
221
+
222
+ await expect(getRegistry().get('bigbasket/cart').func(fakePage, {})).rejects.toThrow(AuthRequiredError);
223
+ await expect(getRegistry().get('bigbasket/checkout').func(fakePage, {})).rejects.toThrow(AuthRequiredError);
224
+ });
225
+
226
+ it('keeps before-checkout recommendations out of cart rows', () => {
227
+ const dom = new JSDOM(`
228
+ <main>
229
+ <section>
230
+ <h1>My Basket</h1>
231
+ <article>
232
+ <a href="/pd/40090893/amul-amul-gold/">Amul Gold Full Cream Milk</a>
233
+ <span>Qty 1</span>
234
+ <span>₹36</span>
235
+ </article>
236
+ </section>
237
+ <section>
238
+ <article>
239
+ <a href="/pd/40321402/rubiks-cube/?nc=beforeyoucheckout">Rubik's Cube</a>
240
+ <span>₹449</span>
241
+ </article>
242
+ </section>
243
+ </main>
244
+ `, { runScripts: 'outside-only', url: 'https://www.bigbasket.com/basket/' });
245
+
246
+ const result = dom.window.eval(CART_EVALUATE);
247
+
248
+ expect(result.rows).toHaveLength(1);
249
+ expect(result.rows[0].product_id).toBe('40090893');
250
+ });
251
+
252
+ it('keeps checkout extractor free of final payment/order submission clicks', () => {
253
+ expect(CHECKOUT_REVIEW_EVALUATE).not.toMatch(/place\\s*order|submit\\s*payment|pay\\s*now|make\\s*payment/i);
254
+ });
255
+ });
@@ -0,0 +1,81 @@
1
+ import { AuthRequiredError, EmptyResultError } from '@agentrhq/webcmd/errors';
2
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
3
+ import { CART_URL, DOMAIN, parseMoney, safeGoto, SITE, toBigBasketUrl } from './utils.js';
4
+
5
+ export const CART_EVALUATE = `
6
+ (() => {
7
+ const clean = (value) => value == null ? '' : String(value).replace(/\\s+/g, ' ').trim();
8
+ const bodyText = clean(document.body?.innerText || document.body?.textContent || '');
9
+ if (/Login\\/ Sign up|Login\\/ Sign Up|Enter Phone number|Using OTP/i.test(bodyText)) {
10
+ return { authRequired: true, rows: [], href: location.href };
11
+ }
12
+ if (!/my basket|basket|cart|checkout|subtotal|total/i.test(bodyText) || /My Smart Basket/i.test(bodyText)) {
13
+ return { rows: [], notCart: true, href: location.href, text: bodyText };
14
+ }
15
+ const itemRoots = Array.from(document.querySelectorAll('[data-testid*="cart"], [class*="Cart"], [class*="cart"], [class*="Basket"], [class*="basket"], li, article'))
16
+ .filter((node) => /₹|rs\\.?|qty|quantity/i.test(clean(node.textContent || '')));
17
+ const seen = new Set();
18
+ const rows = [];
19
+
20
+ for (const root of itemRoots) {
21
+ const bucket = root.closest('section, [class*="Recommendation"], [class*="recommend"], [class*="Carousel"], [class*="carousel"]');
22
+ if (/before you checkout|recommend|you may also like|frequently bought/i.test(clean(bucket?.textContent || ''))) continue;
23
+ const link = root.querySelector('a[href*="/pd/"]');
24
+ const href = link?.href || link?.getAttribute('href') || '';
25
+ if (/beforeyoucheckout/i.test(href)) continue;
26
+ const productId = href.match(/\\/pd\\/(\\d{4,})/)?.[1] || '';
27
+ const title = clean(link?.textContent) || clean(root.querySelector('h1,h2,h3,[class*="name"],[class*="Name"]')?.textContent);
28
+ if (!productId || !title || seen.has(productId)) continue;
29
+ seen.add(productId);
30
+ const text = clean(root.textContent || '');
31
+ const money = text.match(/(?:₹|Rs\\.?)[\\s\\d,.]+/gi) || [];
32
+ const quantity = Number(text.match(/(?:qty|quantity)\\D*(\\d+)/i)?.[1] || 1);
33
+ rows.push({
34
+ product_id: productId,
35
+ title,
36
+ quantity,
37
+ price: money[0] || '',
38
+ line_total: money[money.length - 1] || money[0] || '',
39
+ availability: /out of stock|unavailable/i.test(text) ? 'Out of stock' : '',
40
+ url: href,
41
+ });
42
+ }
43
+ return { rows, href: location.href, text: bodyText };
44
+ })()
45
+ `;
46
+
47
+ cli({
48
+ site: SITE,
49
+ name: 'cart',
50
+ access: 'read',
51
+ description: 'Read BigBasket cart line items',
52
+ domain: DOMAIN,
53
+ strategy: Strategy.COOKIE,
54
+ browser: true,
55
+ args: [],
56
+ columns: ['product_id', 'title', 'quantity', 'price', 'line_total', 'availability', 'url'],
57
+ func: async (page) => {
58
+ await safeGoto(page, CART_URL, 'bigbasket cart');
59
+ if (page.wait) await page.wait(2);
60
+ const result = await page.evaluate(CART_EVALUATE);
61
+ if (result?.authRequired) {
62
+ throw new AuthRequiredError('bigbasket.com', 'Log into BigBasket in the Webcmd browser session to read cart items.');
63
+ }
64
+ const rows = (result?.rows || []).map((row) => ({
65
+ product_id: row.product_id || '',
66
+ title: row.title || '',
67
+ quantity: row.quantity || 1,
68
+ price: parseMoney(row.price),
69
+ line_total: parseMoney(row.line_total),
70
+ availability: row.availability || '',
71
+ url: toBigBasketUrl(row.url),
72
+ })).filter((row) => row.product_id && row.title);
73
+ if (!rows.length) {
74
+ const hint = result?.notCart
75
+ ? 'BigBasket did not expose a cart view in the current session.'
76
+ : 'BigBasket cart is empty or no visible cart items were found.';
77
+ throw new EmptyResultError('bigbasket cart', hint);
78
+ }
79
+ return rows;
80
+ },
81
+ });
@@ -0,0 +1,30 @@
1
+ import { EmptyResultError } from '@agentrhq/webcmd/errors';
2
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
3
+ import { DOMAIN, normalizeProductRow, parseLimitArg, productCardsEvaluate, resolveCategoryUrl, safeGoto, SITE } from './utils.js';
4
+
5
+ cli({
6
+ site: SITE,
7
+ name: 'category',
8
+ access: 'read',
9
+ description: 'Read BigBasket category product cards',
10
+ domain: DOMAIN,
11
+ strategy: Strategy.COOKIE,
12
+ browser: true,
13
+ args: [
14
+ { name: 'category', required: true, positional: true, help: 'Category URL or slug' },
15
+ { name: 'limit', type: 'int', default: 20, help: 'Maximum products to return (max 50)' },
16
+ ],
17
+ columns: ['rank', 'product_id', 'title', 'brand', 'pack_size', 'price', 'mrp', 'discount', 'availability', 'url'],
18
+ func: async (page, kwargs) => {
19
+ const url = resolveCategoryUrl(kwargs.category);
20
+ const limit = parseLimitArg(kwargs.limit, 20, 50);
21
+ await safeGoto(page, url, 'bigbasket category');
22
+ if (page.wait) await page.wait(2);
23
+ const result = await page.evaluate(productCardsEvaluate(limit));
24
+ const rows = (result?.rows || []).map(normalizeProductRow).filter((row) => row.product_id && row.title);
25
+ if (!rows.length) {
26
+ throw new EmptyResultError('bigbasket category', `No BigBasket products found at ${url}.`);
27
+ }
28
+ return rows;
29
+ },
30
+ });
@@ -0,0 +1,71 @@
1
+ import { AuthRequiredError } from '@agentrhq/webcmd/errors';
2
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
3
+ import { CART_URL, DOMAIN, parseMoney, safeGoto, SITE } from './utils.js';
4
+
5
+ export const CHECKOUT_REVIEW_EVALUATE = `
6
+ (async () => {
7
+ const clean = (value) => value == null ? '' : String(value).replace(/\\s+/g, ' ').trim();
8
+ const initialText = clean(document.body?.innerText || '');
9
+ if (/Login\\/ Sign up|Login\\/ Sign Up|Enter Phone number|Using OTP/i.test(initialText)) {
10
+ return { ok: false, stage: 'login', url: location.href };
11
+ }
12
+ const button = Array.from(document.querySelectorAll('button, [role="button"], a')).find((node) => {
13
+ const label = clean(node.textContent || node.getAttribute('aria-label') || node.getAttribute('title'));
14
+ return /checkout|proceed/i.test(label);
15
+ });
16
+ if (button) {
17
+ button.click();
18
+ await new Promise((resolve) => setTimeout(resolve, 2000));
19
+ }
20
+ const text = clean(document.body?.innerText || '');
21
+ const money = text.match(/(?:₹|Rs\\.?)[\\s\\d,.]+/gi) || [];
22
+ const addressReady = /address|deliver/i.test(text) && !/add\\s+address|select\\s+address/i.test(text);
23
+ const deliveryReady = /delivery|slot/i.test(text) && !/select\\s+(?:delivery|slot)/i.test(text);
24
+ const paymentReady = /payment|upi|card|cash/i.test(text);
25
+ const stage = /login|sign\\s*in|mobile number/i.test(text) ? 'login' :
26
+ /address/i.test(text) ? 'address' :
27
+ /delivery|slot/i.test(text) ? 'delivery' :
28
+ /payment|upi|card|cash/i.test(text) ? 'payment-review' :
29
+ 'cart';
30
+ return {
31
+ ok: Boolean(button || /checkout|payment|address|delivery/i.test(text)),
32
+ stage,
33
+ cart_total: money[money.length - 1] || '',
34
+ address_ready: addressReady,
35
+ delivery_ready: deliveryReady,
36
+ payment_ready: paymentReady,
37
+ next_action: paymentReady ? 'Review payment options manually; command stops before final submission.' : 'Complete the visible checkout requirement manually.',
38
+ url: location.href,
39
+ };
40
+ })()
41
+ `;
42
+
43
+ cli({
44
+ site: SITE,
45
+ name: 'checkout',
46
+ access: 'write',
47
+ description: 'Open BigBasket checkout review without placing an order',
48
+ domain: DOMAIN,
49
+ strategy: Strategy.COOKIE,
50
+ browser: true,
51
+ args: [],
52
+ columns: ['ok', 'stage', 'cart_total', 'address_ready', 'delivery_ready', 'payment_ready', 'next_action', 'url'],
53
+ func: async (page) => {
54
+ await safeGoto(page, CART_URL, 'bigbasket checkout');
55
+ if (page.wait) await page.wait(2);
56
+ const result = await page.evaluate(CHECKOUT_REVIEW_EVALUATE);
57
+ if ((result?.stage === 'login' && result?.ok === false) || result?.authRequired === true) {
58
+ throw new AuthRequiredError('bigbasket.com', 'Log into BigBasket in the Webcmd browser session to open checkout review.');
59
+ }
60
+ return [{
61
+ ok: Boolean(result?.ok),
62
+ stage: result?.stage || 'cart',
63
+ cart_total: parseMoney(result?.cart_total),
64
+ address_ready: Boolean(result?.address_ready),
65
+ delivery_ready: Boolean(result?.delivery_ready),
66
+ payment_ready: Boolean(result?.payment_ready),
67
+ next_action: result?.next_action || 'Open BigBasket checkout manually.',
68
+ url: result?.url || CART_URL,
69
+ }];
70
+ },
71
+ });
@@ -0,0 +1,30 @@
1
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
2
+ import { DOMAIN, HOME_URL, normalizeLocationState, safeGoto, SITE } from './utils.js';
3
+
4
+ const LOCATION_EVALUATE = `
5
+ (() => {
6
+ const read = (key) => localStorage.getItem(key) || sessionStorage.getItem(key) || '';
7
+ return {
8
+ selectedAddressInfo: read('selectedAddressInfo'),
9
+ selected_address_id: read('selected_address_id'),
10
+ pin: read('pin') || read('pincode'),
11
+ };
12
+ })()
13
+ `;
14
+
15
+ cli({
16
+ site: SITE,
17
+ name: 'location',
18
+ access: 'read',
19
+ description: 'Show the selected BigBasket delivery location',
20
+ domain: DOMAIN,
21
+ strategy: Strategy.COOKIE,
22
+ browser: true,
23
+ args: [],
24
+ columns: ['selected', 'label', 'area', 'city', 'pincode', 'source'],
25
+ func: async (page) => {
26
+ await safeGoto(page, HOME_URL, 'bigbasket location');
27
+ if (page.wait) await page.wait(1);
28
+ return [normalizeLocationState(await page.evaluate(LOCATION_EVALUATE))];
29
+ },
30
+ });
@@ -0,0 +1,79 @@
1
+ import { CommandExecutionError, EmptyResultError } from '@agentrhq/webcmd/errors';
2
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
3
+ import { DOMAIN, normalizeProductRow, resolveProductInput, safeGoto, SITE } from './utils.js';
4
+
5
+ function productDetailEvaluate(productId) {
6
+ return `
7
+ (() => {
8
+ const productId = ${JSON.stringify(productId)};
9
+ const clean = (value) => value == null ? '' : String(value).replace(/\\s+/g, ' ').trim();
10
+ const jsonLd = Array.from(document.querySelectorAll('script[type="application/ld+json"]'))
11
+ .map((node) => {
12
+ try { return JSON.parse(node.textContent || 'null'); } catch { return null; }
13
+ })
14
+ .flatMap((doc) => Array.isArray(doc) ? doc : [doc])
15
+ .find((doc) => /product/i.test(String(doc?.['@type'] || '')));
16
+ const offers = Array.isArray(jsonLd?.offers) ? jsonLd.offers[0] : jsonLd?.offers;
17
+ const text = clean(document.body?.innerText || '');
18
+ const title = clean(jsonLd?.name) || clean(document.querySelector('h1,h2,[class*="ProductName"],[class*="product-name"]')?.textContent);
19
+ const image = Array.isArray(jsonLd?.image) ? jsonLd.image[0] : jsonLd?.image;
20
+ const priceText = clean(offers?.price) || clean(document.querySelector('[class*="price"], [class*="Price"]')?.textContent);
21
+ const mrpText = clean(document.querySelector('del,[class*="mrp"],[class*="MRP"]')?.textContent);
22
+ const packSize = text.match(/\\b\\d+(?:\\.\\d+)?\\s*(?:kg|g|gm|ml|l|ltr|pcs?|pack)\\b/i)?.[0] || '';
23
+ const availability = /out of stock|unavailable/i.test(text) ? 'Out of stock' : clean(offers?.availability || '');
24
+ const delivery = text.match(/Delivery\\s+in\\s+\\d+\\s*(?:mins?|minutes?|hours?)/i)?.[0] || '';
25
+ return {
26
+ product_id: productId,
27
+ title,
28
+ brand: clean(jsonLd?.brand?.name || jsonLd?.brand),
29
+ pack_size: packSize,
30
+ price: priceText,
31
+ mrp: mrpText,
32
+ discount: text.match(/\\d+%\\s*off/i)?.[0] || '',
33
+ availability,
34
+ delivery,
35
+ image_url: clean(image),
36
+ url: location.href,
37
+ };
38
+ })()
39
+ `;
40
+ }
41
+
42
+ cli({
43
+ site: SITE,
44
+ name: 'product',
45
+ access: 'read',
46
+ description: 'Read BigBasket product details',
47
+ domain: DOMAIN,
48
+ strategy: Strategy.COOKIE,
49
+ browser: true,
50
+ args: [
51
+ { name: 'product', required: true, positional: true, help: 'Product ID or URL' },
52
+ ],
53
+ columns: ['product_id', 'title', 'brand', 'pack_size', 'price', 'mrp', 'discount', 'availability', 'delivery', 'image_url', 'url'],
54
+ func: async (page, kwargs) => {
55
+ const product = resolveProductInput(kwargs.product);
56
+ await safeGoto(page, product.url, 'bigbasket product');
57
+ if (page.wait) await page.wait(2);
58
+ const raw = await page.evaluate(productDetailEvaluate(product.productId)).catch((error) => {
59
+ throw new CommandExecutionError(`bigbasket product extraction failed: ${error?.message || error}`);
60
+ });
61
+ const row = normalizeProductRow(raw, 0);
62
+ if (!row.product_id || !row.title) {
63
+ throw new EmptyResultError('bigbasket product', `No product details found for ${product.productId}.`);
64
+ }
65
+ return [{
66
+ product_id: row.product_id,
67
+ title: row.title,
68
+ brand: row.brand,
69
+ pack_size: row.pack_size,
70
+ price: row.price,
71
+ mrp: row.mrp,
72
+ discount: row.discount,
73
+ availability: row.availability,
74
+ delivery: raw.delivery || '',
75
+ image_url: raw.image_url || '',
76
+ url: row.url,
77
+ }];
78
+ },
79
+ });