@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
@@ -12,6 +12,7 @@ cli({
12
12
  ],
13
13
  columns: ['rank', 'id', 'title', 'subreddit', 'score', 'comments', 'author', 'url', 'created_utc', 'selftext', 'post_hint', 'url_overridden_by_dest', 'preview_image_url', 'gallery_urls'],
14
14
  pipeline: [
15
+ { navigate: 'https://www.reddit.com' },
15
16
  { evaluate: `(async () => {
16
17
  function decodeHtml(s) {
17
18
  if (typeof s !== 'string' || !s) return '';
@@ -43,7 +44,17 @@ cli({
43
44
  const res = await fetch('/r/popular.json?limit=' + limit + '&raw_json=1', {
44
45
  credentials: 'include'
45
46
  });
46
- const d = await res.json();
47
+ const text = await res.text();
48
+ if (!res.ok) {
49
+ throw new Error('Reddit popular request failed: HTTP ' + res.status + ' ' + text.replace(/\\s+/g, ' ').slice(0, 160));
50
+ }
51
+ let d;
52
+ try {
53
+ d = JSON.parse(text);
54
+ } catch {
55
+ const kind = /^\\s*</.test(text) ? 'HTML' : 'non-JSON';
56
+ throw new Error('Reddit popular expected JSON but received ' + kind + ': ' + text.replace(/\\s+/g, ' ').slice(0, 160));
57
+ }
47
58
  return (d?.data?.children || []).map(c => ({
48
59
  id: c.data.id,
49
60
  title: c.data.title,
@@ -4,6 +4,8 @@ import './popular.js';
4
4
 
5
5
  describe('reddit popular adapter', () => {
6
6
  const command = getRegistry().get('reddit/popular');
7
+ const evaluate = command?.pipeline?.find((step) => step.evaluate)?.evaluate;
8
+ const map = command?.pipeline?.find((step) => step.map)?.map;
7
9
 
8
10
  it('exposes the full post-list shape including the 4 media columns', () => {
9
11
  expect(command?.columns).toEqual([
@@ -14,13 +16,20 @@ describe('reddit popular adapter', () => {
14
16
  });
15
17
 
16
18
  it('surfaces media via extractRedditMedia in evaluate + map', () => {
17
- expect(command?.pipeline?.[0]?.evaluate).toContain('function extractRedditMedia');
18
- expect(command?.pipeline?.[0]?.evaluate).toContain('...extractRedditMedia(c.data)');
19
- expect(command?.pipeline?.[1]?.map).toMatchObject({
19
+ expect(evaluate).toContain('function extractRedditMedia');
20
+ expect(evaluate).toContain('...extractRedditMedia(c.data)');
21
+ expect(map).toMatchObject({
20
22
  post_hint: '${{ item.post_hint }}',
21
23
  url_overridden_by_dest: '${{ item.url_overridden_by_dest }}',
22
24
  preview_image_url: '${{ item.preview_image_url }}',
23
25
  gallery_urls: '${{ item.gallery_urls }}',
24
26
  });
25
27
  });
28
+
29
+ it('navigates to Reddit and guards HTML responses before JSON parsing', () => {
30
+ expect(command?.pipeline?.[0]).toEqual({ navigate: 'https://www.reddit.com' });
31
+ expect(evaluate).toContain('await res.text()');
32
+ expect(evaluate).toContain('Reddit popular expected JSON');
33
+ expect(evaluate).not.toContain('await res.json()');
34
+ });
26
35
  });
@@ -0,0 +1,53 @@
1
+ import { CommandExecutionError } from '@agentrhq/webcmd/errors';
2
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
3
+ import { CART_EVALUATE, DOMAIN, SITE, ZEPTO_NAV_OPTIONS, parseQuantityArg, resolveProductInput, safeGoto } from './utils.js';
4
+
5
+ function addToCartEvaluate(quantity) {
6
+ return `
7
+ (async () => {
8
+ const clean = (value) => value == null ? '' : String(value).replace(/\\s+/g, ' ').trim();
9
+ const findButton = (pattern) => Array.from(document.querySelectorAll('button, [role="button"]')).find((node) => pattern.test(clean(node.innerText || node.textContent || node.getAttribute('aria-label'))));
10
+ let button = findButton(/^(ADD|Add to Cart)$/i);
11
+ if (!button) return { ok: false, message: 'ADD_BUTTON_NOT_FOUND' };
12
+ button.click();
13
+ await new Promise((resolve) => setTimeout(resolve, 800));
14
+ for (let i = 1; i < ${Number(quantity) || 1}; i += 1) {
15
+ const plus = findButton(/increase quantity by one|\\+|add/i);
16
+ plus?.click();
17
+ await new Promise((resolve) => setTimeout(resolve, 300));
18
+ }
19
+ const cart = (${CART_EVALUATE});
20
+ const itemCount = (cart.rows || []).reduce((sum, row) => sum + Number(row.quantity || 0), 0);
21
+ return { ok: true, message: 'Added to cart', item_count: itemCount };
22
+ })()
23
+ `;
24
+ }
25
+
26
+ cli({
27
+ site: SITE,
28
+ name: 'add-to-cart',
29
+ access: 'write',
30
+ description: 'Add a Zepto product to cart',
31
+ domain: DOMAIN,
32
+ strategy: Strategy.COOKIE,
33
+ browser: true,
34
+ navigateBefore: false,
35
+ args: [
36
+ { name: 'product', required: true, positional: true, help: 'Product URL from Zepto search results' },
37
+ { name: 'quantity', type: 'int', default: 1, help: 'Quantity to add (max 12)' },
38
+ ],
39
+ columns: ['ok', 'product_id', 'quantity', 'item_count', 'message'],
40
+ func: async (page, kwargs) => {
41
+ const product = resolveProductInput(kwargs.product);
42
+ const quantity = parseQuantityArg(kwargs.quantity, 1, 12);
43
+ await safeGoto(page, product.url, 'zepto add-to-cart', ZEPTO_NAV_OPTIONS);
44
+ if (page.wait) await page.wait(2);
45
+ const result = await page.evaluate(addToCartEvaluate(quantity)).catch((error) => {
46
+ throw new CommandExecutionError(`zepto add-to-cart failed: ${error?.message || error}`);
47
+ });
48
+ if (!result?.ok) throw new CommandExecutionError(result?.message === 'ADD_BUTTON_NOT_FOUND' ? 'Could not find a Zepto add-to-cart button.' : 'Failed to add Zepto item to cart.');
49
+ return [{ ok: true, product_id: product.productId, quantity, item_count: Number(result.item_count || 0), message: 'Added to cart' }];
50
+ },
51
+ });
52
+
53
+ export const __test__ = { addToCartEvaluate };
@@ -0,0 +1,59 @@
1
+ import { AuthRequiredError, TimeoutError } from '@agentrhq/webcmd/errors';
2
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
3
+ import { DOMAIN, HOME_URL, SITE, ZEPTO_NAV_OPTIONS, safeGoto } from './utils.js';
4
+
5
+ function authEvaluate() {
6
+ return `
7
+ (() => {
8
+ const text = document.body?.innerText || '';
9
+ const loggedIn = !/\\bLogin\\b/i.test(text) && !/please login/i.test(text);
10
+ return { loggedIn };
11
+ })()
12
+ `;
13
+ }
14
+
15
+ function openLoginEvaluate() {
16
+ return `
17
+ (() => {
18
+ const buttons = Array.from(document.querySelectorAll('button, [role="button"], a'));
19
+ const button = buttons.find((node) => /\\blogin\\b/i.test(node.innerText || node.textContent || node.getAttribute('aria-label') || ''));
20
+ button?.dispatchEvent?.(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
21
+ return Boolean(button);
22
+ })()
23
+ `;
24
+ }
25
+
26
+ cli({
27
+ site: SITE,
28
+ name: 'login',
29
+ access: 'write',
30
+ description: 'Open Zepto login and wait until the browser session is authenticated',
31
+ domain: DOMAIN,
32
+ strategy: Strategy.COOKIE,
33
+ browser: true,
34
+ navigateBefore: false,
35
+ defaultWindowMode: 'foreground',
36
+ siteSession: 'persistent',
37
+ args: [
38
+ { name: 'timeout', type: 'int', default: 300, help: 'Maximum seconds to wait for the user to finish login' },
39
+ ],
40
+ columns: ['status', 'logged_in', 'site'],
41
+ func: async (page, kwargs) => {
42
+ await safeGoto(page, HOME_URL, 'zepto login', ZEPTO_NAV_OPTIONS);
43
+ if ((await page.evaluate(authEvaluate()).catch(() => ({ loggedIn: false }))).loggedIn) {
44
+ return [{ status: 'already_logged_in', logged_in: true, site: SITE }];
45
+ }
46
+ await page.evaluate(openLoginEvaluate()).catch(() => false);
47
+ const timeout = Number(kwargs.timeout ?? 300);
48
+ const deadline = Date.now() + timeout * 1000;
49
+ while (Date.now() < deadline) {
50
+ await page.wait(2);
51
+ if ((await page.evaluate(authEvaluate()).catch(() => ({ loggedIn: false }))).loggedIn) {
52
+ return [{ status: 'login_complete', logged_in: true, site: SITE }];
53
+ }
54
+ }
55
+ throw new TimeoutError('zepto login', timeout, 'Run webcmd zepto login and complete the login dialog in the browser.');
56
+ },
57
+ });
58
+
59
+ export const __test__ = { authEvaluate, openLoginEvaluate };
@@ -0,0 +1,23 @@
1
+ import { EmptyResultError } from '@agentrhq/webcmd/errors';
2
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
3
+ import { CART_EVALUATE, DOMAIN, HOME_URL, SITE, ZEPTO_NAV_OPTIONS, normalizeCartRows, safeGoto } from './utils.js';
4
+
5
+ cli({
6
+ site: SITE,
7
+ name: 'cart',
8
+ access: 'read',
9
+ description: 'Read Zepto cart line items',
10
+ domain: DOMAIN,
11
+ strategy: Strategy.COOKIE,
12
+ browser: true,
13
+ navigateBefore: false,
14
+ args: [],
15
+ columns: ['rank', 'product_id', 'title', 'pack_size', 'quantity', 'price', 'mrp', 'availability'],
16
+ func: async (page) => {
17
+ await safeGoto(page, HOME_URL, 'zepto cart', ZEPTO_NAV_OPTIONS);
18
+ if (page.wait) await page.wait(1);
19
+ const rows = normalizeCartRows(await page.evaluate(CART_EVALUATE));
20
+ if (!rows.length) throw new EmptyResultError('zepto cart', 'Zepto cart is empty or no visible cart items were found.');
21
+ return rows;
22
+ },
23
+ });
@@ -0,0 +1,60 @@
1
+ import { AuthRequiredError } from '@agentrhq/webcmd/errors';
2
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
3
+ import { CART_EVALUATE, DOMAIN, HOME_URL, SITE, ZEPTO_NAV_OPTIONS, safeGoto } from './utils.js';
4
+
5
+ const CHECKOUT_EVALUATE = `
6
+ (async () => {
7
+ const clean = (value) => value == null ? '' : String(value).replace(/\\s+/g, ' ').trim();
8
+ const bodyText = clean(document.body?.innerText || document.body?.textContent || '');
9
+ if (/please login|\\bLogin\\b/i.test(bodyText) && /cart is empty|please login/i.test(bodyText)) {
10
+ return { ok: false, stage: 'login', url: location.href };
11
+ }
12
+ const cartButton = Array.from(document.querySelectorAll('button, [role="button"], a')).find((node) => /\\bCart\\b/i.test(clean(node.innerText || node.textContent || node.getAttribute('aria-label'))));
13
+ cartButton?.click();
14
+ await new Promise((resolve) => setTimeout(resolve, 1200));
15
+ const text = clean(document.body?.innerText || document.body?.textContent || '');
16
+ const checkoutButton = Array.from(document.querySelectorAll('button, [role="button"], a')).find((node) => /checkout|proceed|continue/i.test(clean(node.innerText || node.textContent || node.getAttribute('aria-label'))));
17
+ checkoutButton?.click();
18
+ await new Promise((resolve) => setTimeout(resolve, 1200));
19
+ const finalText = clean(document.body?.innerText || document.body?.textContent || '');
20
+ const cart = (${CART_EVALUATE});
21
+ return {
22
+ ok: Boolean(cartButton || checkoutButton || /checkout|payment|address|delivery/i.test(finalText || text)),
23
+ stage: /login|sign\\s*in|please login/i.test(finalText) ? 'login' :
24
+ /address/i.test(finalText) ? 'address' :
25
+ /payment|upi|card|cash/i.test(finalText) ? 'payment-review' :
26
+ /cart is empty/i.test(finalText) ? 'empty' :
27
+ 'cart',
28
+ url: location.href,
29
+ item_count: (cart.rows || []).reduce((sum, row) => sum + Number(row.quantity || 0), 0),
30
+ };
31
+ })()
32
+ `;
33
+
34
+ cli({
35
+ site: SITE,
36
+ name: 'checkout',
37
+ access: 'write',
38
+ description: 'Open Zepto checkout review without placing an order',
39
+ domain: DOMAIN,
40
+ strategy: Strategy.COOKIE,
41
+ browser: true,
42
+ navigateBefore: false,
43
+ args: [],
44
+ columns: ['ok', 'stage', 'item_count', 'next_action', 'url'],
45
+ func: async (page) => {
46
+ await safeGoto(page, HOME_URL, 'zepto checkout', ZEPTO_NAV_OPTIONS);
47
+ if (page.wait) await page.wait(1);
48
+ const result = await page.evaluate(CHECKOUT_EVALUATE);
49
+ if (result?.stage === 'login') throw new AuthRequiredError(DOMAIN, 'Log into Zepto in the Webcmd browser session to open checkout.');
50
+ return [{
51
+ ok: Boolean(result?.ok),
52
+ stage: result?.stage || 'cart',
53
+ item_count: Number(result?.item_count || 0),
54
+ next_action: 'Complete visible checkout requirements manually; command stops before final submission.',
55
+ url: result?.url || HOME_URL,
56
+ }];
57
+ },
58
+ });
59
+
60
+ export const __test__ = { CHECKOUT_EVALUATE };
@@ -0,0 +1,20 @@
1
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
2
+ import { DOMAIN, HOME_URL, SITE, ZEPTO_NAV_OPTIONS, normalizeLocationState, readLocationEvaluate, safeGoto } from './utils.js';
3
+
4
+ cli({
5
+ site: SITE,
6
+ name: 'location',
7
+ access: 'read',
8
+ description: 'Show the selected Zepto delivery location',
9
+ domain: DOMAIN,
10
+ strategy: Strategy.COOKIE,
11
+ browser: true,
12
+ navigateBefore: false,
13
+ args: [],
14
+ columns: ['selected', 'label', 'area', 'city', 'pincode', 'hasCoordinates', 'source'],
15
+ func: async (page) => {
16
+ await safeGoto(page, HOME_URL, 'zepto location', ZEPTO_NAV_OPTIONS);
17
+ if (page.wait) await page.wait(1);
18
+ return [normalizeLocationState(await page.evaluate(readLocationEvaluate()))];
19
+ },
20
+ });
@@ -0,0 +1,47 @@
1
+ import { CommandExecutionError } from '@agentrhq/webcmd/errors';
2
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
3
+ import { DOMAIN, HOME_URL, SITE, ZEPTO_NAV_OPTIONS, safeGoto } from './utils.js';
4
+
5
+ function parseConfirm(raw) {
6
+ return raw === true || raw === 'true' || raw === '1' || raw === 1;
7
+ }
8
+
9
+ function placeOrderEvaluate() {
10
+ return `
11
+ (() => {
12
+ const clean = (value) => value == null ? '' : String(value).replace(/\\s+/g, ' ').trim();
13
+ const button = Array.from(document.querySelectorAll('button, [role="button"]')).find((node) => /^(place order|pay now|make payment)$/i.test(clean(node.innerText || node.textContent || node.getAttribute('aria-label'))));
14
+ if (!button) return { ok: false, status: 'blocked', message: 'No final place-order/payment button is visible.' };
15
+ button.click();
16
+ return { ok: true, status: 'submitted', message: 'Clicked final Zepto order/payment button.' };
17
+ })()
18
+ `;
19
+ }
20
+
21
+ cli({
22
+ site: SITE,
23
+ name: 'place-order',
24
+ access: 'write',
25
+ description: 'Submit a real Zepto order only when --confirm true is passed',
26
+ domain: DOMAIN,
27
+ strategy: Strategy.COOKIE,
28
+ browser: true,
29
+ navigateBefore: false,
30
+ args: [
31
+ { name: 'confirm', type: 'boolean', default: false, help: 'Required. Set true to submit a real Zepto order/payment action.' },
32
+ ],
33
+ columns: ['status', 'confirmed', 'message'],
34
+ func: async (page, kwargs) => {
35
+ if (!parseConfirm(kwargs.confirm)) {
36
+ return [{ status: 'no-op', confirmed: false, message: 'Pass --confirm true to submit a real Zepto order/payment action.' }];
37
+ }
38
+ await safeGoto(page, HOME_URL, 'zepto place-order', ZEPTO_NAV_OPTIONS);
39
+ const result = await page.evaluate(placeOrderEvaluate()).catch((error) => {
40
+ throw new CommandExecutionError(`zepto place-order failed: ${error?.message || error}`);
41
+ });
42
+ if (!result?.ok) throw new CommandExecutionError(result?.message || 'No final Zepto place-order/payment button is visible.');
43
+ return [{ status: result.status || 'submitted', confirmed: true, message: result.message || 'Submitted Zepto order.' }];
44
+ },
45
+ });
46
+
47
+ export const __test__ = { placeOrderEvaluate };
@@ -0,0 +1,52 @@
1
+ import { EmptyResultError } from '@agentrhq/webcmd/errors';
2
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
3
+ import { DOMAIN, SITE, ZEPTO_NAV_OPTIONS, cleanText, normalizeProductRow, resolveProductInput, safeGoto } from './utils.js';
4
+
5
+ function productEvaluate(productId) {
6
+ return `
7
+ (() => {
8
+ const productId = ${JSON.stringify(productId)};
9
+ const clean = (value) => value == null ? '' : String(value).replace(/\\s+/g, ' ').trim();
10
+ const text = clean(document.body?.innerText || document.body?.textContent || '');
11
+ const prices = Array.from(document.querySelectorAll('span,div'))
12
+ .map((node) => clean(node.textContent))
13
+ .filter((value) => /^₹\\s*\\d/i.test(value));
14
+ const title = clean(document.querySelector('h1')?.textContent)
15
+ || text.split('\\n').map(clean).find((line) => /[A-Za-z]/.test(line) && !/Delivery|Search|Login|Cart|Home|₹|Add to Cart/i.test(line))
16
+ || '';
17
+ return {
18
+ product_id: productId,
19
+ title,
20
+ brand: clean(Array.from(document.querySelectorAll('div,span')).find((node) => clean(node.textContent) === 'Brand')?.nextElementSibling?.textContent),
21
+ pack_size: text.match(/\\b\\d+(?:\\.\\d+)?\\s*(?:kg|g|ml|l|pcs?|pack)\\b/i)?.[0] || '',
22
+ price: prices[0] || '',
23
+ mrp: prices[1] || '',
24
+ availability: /out of stock|unavailable/i.test(text) ? 'Out of stock' : '',
25
+ url: location.href,
26
+ };
27
+ })()
28
+ `;
29
+ }
30
+
31
+ cli({
32
+ site: SITE,
33
+ name: 'product',
34
+ access: 'read',
35
+ description: 'Read Zepto product details',
36
+ domain: DOMAIN,
37
+ strategy: Strategy.COOKIE,
38
+ browser: true,
39
+ navigateBefore: false,
40
+ args: [
41
+ { name: 'product', required: true, positional: true, help: 'Product URL from Zepto search results' },
42
+ ],
43
+ columns: ['product_id', 'title', 'brand', 'pack_size', 'price', 'mrp', 'availability', 'url'],
44
+ func: async (page, kwargs) => {
45
+ const product = resolveProductInput(kwargs.product);
46
+ await safeGoto(page, product.url, 'zepto product', ZEPTO_NAV_OPTIONS);
47
+ if (page.wait) await page.wait(2);
48
+ const row = normalizeProductRow(await page.evaluate(productEvaluate(product.productId)), 0);
49
+ if (!row.product_id || !cleanText(row.title)) throw new EmptyResultError('zepto product', `No product details found for ${product.productId}.`);
50
+ return [row];
51
+ },
52
+ });
@@ -0,0 +1,30 @@
1
+ import { AuthRequiredError, EmptyResultError } from '@agentrhq/webcmd/errors';
2
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
3
+ import { DOMAIN, SITE, ZEPTO_NAV_OPTIONS, buildSearchUrl, normalizeProductRow, parseLimitArg, productCardsEvaluate, safeGoto } from './utils.js';
4
+
5
+ cli({
6
+ site: SITE,
7
+ name: 'search',
8
+ access: 'read',
9
+ description: 'Search Zepto products',
10
+ domain: DOMAIN,
11
+ strategy: Strategy.COOKIE,
12
+ browser: true,
13
+ navigateBefore: false,
14
+ args: [
15
+ { name: 'query', required: true, positional: true, help: 'Search query' },
16
+ { name: 'limit', type: 'int', default: 20, help: 'Maximum products to return (max 50)' },
17
+ ],
18
+ columns: ['rank', 'product_id', 'title', 'brand', 'pack_size', 'price', 'mrp', 'availability', 'url'],
19
+ func: async (page, kwargs) => {
20
+ const url = buildSearchUrl(kwargs.query);
21
+ const limit = parseLimitArg(kwargs.limit, 20, 50);
22
+ await safeGoto(page, url, 'zepto search', ZEPTO_NAV_OPTIONS);
23
+ if (page.wait) await page.wait(2);
24
+ const result = await page.evaluate(productCardsEvaluate(limit));
25
+ if (result?.authRequired) throw new AuthRequiredError(DOMAIN, 'Log into Zepto in the Webcmd browser session to search products.');
26
+ const rows = (result?.rows || []).map(normalizeProductRow).filter((row) => row.product_id && row.title);
27
+ if (!rows.length) throw new EmptyResultError('zepto search', `No Zepto products matched "${kwargs.query}".`);
28
+ return rows;
29
+ },
30
+ });
@@ -0,0 +1,228 @@
1
+ import { ArgumentError, CommandExecutionError } from '@agentrhq/webcmd/errors';
2
+
3
+ export const SITE = 'zepto';
4
+ export const DOMAIN = 'www.zepto.com';
5
+ export const HOME_URL = 'https://www.zepto.com';
6
+ export const MAX_LIMIT = 50;
7
+ export const ZEPTO_NAV_OPTIONS = { waitUntil: 'none', settleMs: 1500 };
8
+
9
+ export function cleanText(value) {
10
+ return typeof value === 'string'
11
+ ? value.replace(/\u00a0/g, ' ').replace(/\s+/g, ' ').trim()
12
+ : '';
13
+ }
14
+
15
+ export function parseMoney(value) {
16
+ if (typeof value === 'number' && Number.isFinite(value)) return value > 999 ? value / 100 : value;
17
+ const match = cleanText(String(value ?? '')).replace(/,/g, '').match(/(\d+(?:\.\d+)?)/);
18
+ return match ? Number(match[1]) : null;
19
+ }
20
+
21
+ export function parseLimitArg(raw, fallback, max = MAX_LIMIT) {
22
+ if (raw === undefined || raw === null || raw === '') return fallback;
23
+ const num = Number(raw);
24
+ if (!Number.isInteger(num) || num < 1 || num > max) {
25
+ throw new ArgumentError(`--limit must be an integer between 1 and ${max} (got ${raw})`);
26
+ }
27
+ return num;
28
+ }
29
+
30
+ export function parseQuantityArg(raw, fallback, max = 12) {
31
+ if (raw === undefined || raw === null || raw === '') return fallback;
32
+ const num = Number(raw);
33
+ if (!Number.isInteger(num) || num < 1 || num > max) {
34
+ throw new ArgumentError(`--quantity must be an integer between 1 and ${max} (got ${raw})`);
35
+ }
36
+ return num;
37
+ }
38
+
39
+ export function buildSearchUrl(query) {
40
+ const normalized = cleanText(query);
41
+ if (!normalized) throw new ArgumentError('zepto search query cannot be empty');
42
+ return `${HOME_URL}/search?query=${encodeURIComponent(normalized)}`;
43
+ }
44
+
45
+ export function toZeptoUrl(value) {
46
+ const text = cleanText(value);
47
+ if (!text) return '';
48
+ try {
49
+ const url = new URL(text.startsWith('http') ? text : text.startsWith('/') ? text : `/${text}`, HOME_URL);
50
+ if (url.hostname !== DOMAIN && url.hostname !== 'zepto.com') throw new Error('not zepto');
51
+ url.hostname = DOMAIN;
52
+ url.hash = '';
53
+ return url.toString();
54
+ } catch {
55
+ return '';
56
+ }
57
+ }
58
+
59
+ export function resolveProductInput(input) {
60
+ const text = cleanText(input);
61
+ if (!text) throw new ArgumentError('zepto product requires a product URL');
62
+ const url = toZeptoUrl(text);
63
+ const match = url && new URL(url).pathname.match(/\/pvid\/([0-9a-f-]{36})(?:\/|$)/i);
64
+ if (!match) throw new ArgumentError('zepto product expects a Zepto /pn/.../pvid/<id> URL from search results');
65
+ return { productId: match[1], url };
66
+ }
67
+
68
+ export function normalizeProductRow(raw, rank) {
69
+ const url = toZeptoUrl(raw.url || raw.href || '');
70
+ const productId = cleanText(raw.product_id || raw.productId || raw.id || url.match(/\/pvid\/([0-9a-f-]{36})/i)?.[1] || '');
71
+ return {
72
+ rank: rank + 1,
73
+ product_id: productId,
74
+ title: cleanText(raw.title || raw.name),
75
+ brand: cleanText(raw.brand),
76
+ pack_size: cleanText(raw.pack_size || raw.packSize || raw.size),
77
+ price: parseMoney(raw.price),
78
+ mrp: parseMoney(raw.mrp || raw.original_price || raw.originalPrice),
79
+ availability: cleanText(raw.availability || raw.stock),
80
+ url,
81
+ };
82
+ }
83
+
84
+ function parsePersisted(raw) {
85
+ try {
86
+ const outer = typeof raw === 'string' ? JSON.parse(raw) : raw;
87
+ const value = typeof outer?.value === 'string' ? JSON.parse(outer.value) : outer;
88
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
89
+ } catch {
90
+ return {};
91
+ }
92
+ }
93
+
94
+ export function normalizeLocationState(raw = {}) {
95
+ const rawAddressText = cleanText(raw.addressText);
96
+ const addressText = /^select location$/i.test(rawAddressText) ? '' : rawAddressText;
97
+ const rawLabel = addressText.match(/^(Home|Work|Other)\b/i)?.[1] || '';
98
+ const label = rawLabel ? rawLabel[0].toUpperCase() + rawLabel.slice(1).toLowerCase() : '';
99
+ const publicAddressText = addressText
100
+ .replace(/^(Home|Work|Other)\s*[:-]?\s*/i, '')
101
+ .replace(/\b(?:house\s*number|floor|flat|apartment|door|building)\b\s*[-:#]?\s*[^,•\n]*/gi, '');
102
+ const parts = publicAddressText.split(/[,•\n]/).map(cleanText).filter(Boolean);
103
+ const position = raw.userPosition || raw.userGpsCoords || {};
104
+ const pincode = cleanText(raw.pincode || raw.pin || addressText.match(/\b\d{6}\b/)?.[0]);
105
+ const cityPart = parts.length > 1 ? (parts[parts.length - 1].toLowerCase() === 'india' && parts.length > 2 ? parts[parts.length - 2] : parts[parts.length - 1]) : cleanText(raw.city);
106
+ return {
107
+ selected: Boolean(addressText || position.latitude || position.longitude || raw.pincode),
108
+ label,
109
+ area: label ? '' : parts[0] || '',
110
+ city: cleanText(cityPart.replace(/\b\d{6}\b/g, '')),
111
+ pincode,
112
+ hasCoordinates: Boolean(position.latitude || position.longitude),
113
+ source: 'browser',
114
+ };
115
+ }
116
+
117
+ export async function safeGoto(page, url, label, options) {
118
+ await page.goto(url, options || ZEPTO_NAV_OPTIONS).catch((error) => {
119
+ const message = error?.message || String(error);
120
+ if (/net::ERR_ABORTED|Browser navigate command timed out/i.test(message)) return;
121
+ throw new CommandExecutionError(`${label} navigation failed: ${error?.message || error}`);
122
+ });
123
+ }
124
+
125
+ export function productCardsEvaluate(limit) {
126
+ return `
127
+ (() => {
128
+ const limit = ${Number(limit) || 20};
129
+ const clean = (value) => value == null ? '' : String(value).replace(/\\s+/g, ' ').trim();
130
+ const bodyText = clean(document.body?.innerText || document.body?.textContent || '');
131
+ if (/please login to continue searching|please login/i.test(bodyText) && location.pathname.includes('/search')) {
132
+ return { authRequired: true, rows: [], href: location.href };
133
+ }
134
+ const rows = [];
135
+ const seen = new Set();
136
+ const anchors = Array.from(document.querySelectorAll('a[href*="/pn/"][href*="/pvid/"]'));
137
+ for (const anchor of anchors) {
138
+ const href = anchor.href || anchor.getAttribute('href') || '';
139
+ const productId = href.match(/\\/pvid\\/([0-9a-f-]{36})/i)?.[1] || '';
140
+ if (!productId || seen.has(productId)) continue;
141
+ let root = anchor;
142
+ for (let i = 0; i < 6 && root?.parentElement; i += 1) {
143
+ const rootText = clean(root.innerText || root.textContent || '');
144
+ if (/₹|ADD|Add to Cart/i.test(rootText)) break;
145
+ root = root.parentElement;
146
+ }
147
+ const texts = Array.from(root.querySelectorAll('*'))
148
+ .filter((node) => node.children.length === 0)
149
+ .map((node) => clean(node.textContent))
150
+ .filter(Boolean);
151
+ const joined = texts.join(' ');
152
+ const prices = texts.filter((text) => /₹/.test(text));
153
+ const title = texts.find((text) => /[A-Za-z]/.test(text) && !/add|cart|off|bestseller|₹|\\d+(?:\\.\\d+)?\\s*(?:kg|g|ml|l|pcs?|pack)/i.test(text)) || clean(anchor.textContent);
154
+ if (!title) continue;
155
+ seen.add(productId);
156
+ rows.push({
157
+ product_id: productId,
158
+ title,
159
+ pack_size: texts.find((text) => /\\b\\d+(?:\\.\\d+)?\\s*(?:kg|g|ml|l|pcs?|pack)\\b/i.test(text)) || '',
160
+ price: prices[0] || '',
161
+ mrp: prices[1] || '',
162
+ availability: /out of stock|unavailable/i.test(joined) ? 'Out of stock' : '',
163
+ url: href,
164
+ });
165
+ if (rows.length >= limit) break;
166
+ }
167
+ return { rows, href: location.href };
168
+ })()
169
+ `;
170
+ }
171
+
172
+ export const CART_EVALUATE = `
173
+ (() => {
174
+ const clean = (value) => value == null ? '' : String(value).replace(/\\s+/g, ' ').trim();
175
+ const parse = (key) => {
176
+ try { return JSON.parse(localStorage.getItem(key) || 'null'); } catch { return null; }
177
+ };
178
+ const cart = parse('cart')?.state || {};
179
+ const content = cart.cartContent || {};
180
+ const rows = Object.entries(content).map(([id, item]) => {
181
+ const product = item?.productVariant || item?.product || item || {};
182
+ const productId = clean(product.id || product.productVariantId || item?.productVariantId || id);
183
+ const price = Number(item?.superSaverSellingPrice ?? item?.discountedSellingPrice ?? item?.sellingPrice ?? product.price ?? product.discountedSellingPrice ?? product.sellingPrice);
184
+ const mrp = Number(item?.mrp ?? product.mrp ?? product.mrpPrice);
185
+ return {
186
+ product_id: productId,
187
+ title: clean(product.name || product.productName || item?.title || item?.name),
188
+ pack_size: clean(product.formattedPacksize || product.packsize || product.unitOfMeasure || item?.quantityText),
189
+ quantity: Number(item?.quantity || item?.qty || 0),
190
+ price: Number.isFinite(price) ? (price > 999 ? price / 100 : price) : null,
191
+ mrp: Number.isFinite(mrp) ? (mrp > 999 ? mrp / 100 : mrp) : null,
192
+ availability: clean(product.availability || ''),
193
+ };
194
+ }).filter((row) => row.product_id && row.title && row.quantity);
195
+ return { rows, href: location.href, cartId: cart.cartId || '' };
196
+ })()
197
+ `;
198
+
199
+ export function normalizeCartRows(result) {
200
+ return (result?.rows || []).map((row, index) => ({
201
+ rank: index + 1,
202
+ product_id: cleanText(row.product_id),
203
+ title: cleanText(row.title),
204
+ pack_size: cleanText(row.pack_size),
205
+ quantity: Number(row.quantity || 0),
206
+ price: parseMoney(row.price),
207
+ mrp: parseMoney(row.mrp),
208
+ availability: cleanText(row.availability),
209
+ })).filter((row) => row.product_id && row.title && row.quantity);
210
+ }
211
+
212
+ export function readLocationEvaluate() {
213
+ return `
214
+ (() => {
215
+ const clean = (value) => value == null ? '' : String(value).replace(/\\s+/g, ' ').trim();
216
+ const parse = (key) => {
217
+ try { return JSON.parse(localStorage.getItem(key) || 'null'); } catch { return null; }
218
+ };
219
+ const persisted = parse('user-position');
220
+ const state = typeof persisted?.value === 'string' ? JSON.parse(persisted.value)?.state : persisted?.state;
221
+ return {
222
+ addressText: clean(document.querySelector('[data-testid="user-address"]')?.textContent || ''),
223
+ userPosition: state?.userPosition || null,
224
+ userGpsCoords: state?.userGpsCoords || null,
225
+ };
226
+ })()
227
+ `;
228
+ }