@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
@@ -0,0 +1,32 @@
1
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
2
+ import { DOMAIN, openCartPanel, readCartState, summarizeCartResponse } from './utils.js';
3
+
4
+ cli({
5
+ site: 'blinkit',
6
+ name: 'checkout',
7
+ access: 'read',
8
+ description: 'Review Blinkit checkout totals and blockers without placing an order',
9
+ domain: DOMAIN,
10
+ strategy: Strategy.COOKIE,
11
+ browser: true,
12
+ navigateBefore: false,
13
+ defaultWindowMode: 'foreground',
14
+ args: [],
15
+ columns: ['status', 'itemCount', 'itemsTotal', 'deliveryCharge', 'handlingCharge', 'payable', 'cartState', 'checkoutBlocked', 'validations'],
16
+ func: async (page) => {
17
+ await openCartPanel(page);
18
+ const state = await readCartState(page);
19
+ const summary = summarizeCartResponse(state);
20
+ return [{
21
+ status: state.loggedIn ? summary.status : 'login_required',
22
+ itemCount: summary.itemCount,
23
+ itemsTotal: summary.itemsTotal,
24
+ deliveryCharge: summary.deliveryCharge,
25
+ handlingCharge: summary.handlingCharge,
26
+ payable: summary.payable,
27
+ cartState: summary.cartState,
28
+ checkoutBlocked: summary.checkoutBlocked,
29
+ validations: summary.validations,
30
+ }];
31
+ },
32
+ });
@@ -0,0 +1,29 @@
1
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
2
+ import { DOMAIN, gotoBlinkit, normalizeLocationState } from './utils.js';
3
+
4
+ const LOCATION_EVALUATE = `
5
+ (() => {
6
+ const readJson = (key) => {
7
+ try { return JSON.parse(localStorage.getItem(key) || 'null'); } catch { return null; }
8
+ };
9
+ return window.__reduxStore__?.getState?.()?.data?.location || readJson('location') || {};
10
+ })()
11
+ `;
12
+
13
+ cli({
14
+ site: 'blinkit',
15
+ name: 'location',
16
+ access: 'read',
17
+ description: 'Show the selected Blinkit delivery location',
18
+ domain: DOMAIN,
19
+ strategy: Strategy.COOKIE,
20
+ browser: true,
21
+ args: [],
22
+ columns: ['selected', 'label', 'area', 'city', 'pincode', 'hasCoordinates', 'source'],
23
+ func: async (page) => {
24
+ await gotoBlinkit(page, '/');
25
+ if (page.wait) await page.wait(1);
26
+ return [normalizeLocationState(await page.evaluate(LOCATION_EVALUATE))];
27
+ },
28
+ });
29
+
@@ -0,0 +1,78 @@
1
+ import { ArgumentError, CommandExecutionError } from '@agentrhq/webcmd/errors';
2
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
3
+ import { DOMAIN, ensureCartHasItems, ensureLoggedIn, openCartPanel, readCartState, summarizeCartResponse } from './utils.js';
4
+
5
+ function buildPlaceOrderEvaluate() {
6
+ return `
7
+ (async () => {
8
+ const finalLabels = [/^place order$/i, /^pay( now)?$/i, /^cash on delivery$/i];
9
+ const buttons = Array.from(document.querySelectorAll('button, [role="button"], a'));
10
+ const target = buttons.find((node) => {
11
+ const text = (node.innerText || node.textContent || '').trim().replace(/\\s+/g, ' ');
12
+ return text && finalLabels.some((pattern) => pattern.test(text));
13
+ });
14
+ if (!target) {
15
+ return { ok: false, status: 'blocked', message: 'No final place-order/payment button is visible. Complete address/payment selection in the browser checkout first.' };
16
+ }
17
+ target.click();
18
+ await new Promise((resolve) => setTimeout(resolve, 3500));
19
+ const text = document.body.innerText || '';
20
+ const orderMatch = text.match(/order(?:\\s+id)?[:#\\s-]*([A-Z0-9-]{6,})/i);
21
+ if (/payment failed|try again|could not/i.test(text)) {
22
+ return { ok: false, status: 'failed', message: 'Blinkit reported a payment/order failure', url: location.href };
23
+ }
24
+ return { ok: true, status: orderMatch ? 'placed' : 'submitted', orderId: orderMatch?.[1] || '', url: location.href };
25
+ })()
26
+ `;
27
+ }
28
+
29
+ cli({
30
+ site: 'blinkit',
31
+ name: 'place-order',
32
+ access: 'write',
33
+ description: 'Submit the visible Blinkit final order/payment action. Requires --confirm.',
34
+ domain: DOMAIN,
35
+ strategy: Strategy.COOKIE,
36
+ browser: true,
37
+ navigateBefore: false,
38
+ defaultWindowMode: 'foreground',
39
+ args: [
40
+ { name: 'confirm', type: 'bool', default: false, help: 'Required acknowledgement that this may place/pay for a real order' },
41
+ ],
42
+ columns: ['status', 'confirmed', 'itemCount', 'payable', 'orderId', 'url', 'message'],
43
+ func: async (page, kwargs) => {
44
+ if (!kwargs.confirm) {
45
+ return [{
46
+ status: 'no-op',
47
+ confirmed: false,
48
+ message: 'Pass --confirm to submit a real Blinkit order/payment action.',
49
+ }];
50
+ }
51
+ if (kwargs.confirm !== true) throw new ArgumentError('--confirm must be a boolean flag');
52
+
53
+ await openCartPanel(page);
54
+ const state = await readCartState(page);
55
+ ensureLoggedIn(state, 'blinkit place-order');
56
+ const summary = summarizeCartResponse(state);
57
+ ensureCartHasItems(summary);
58
+ if (summary.checkoutBlocked) throw new CommandExecutionError('Blinkit checkout is blocked for this cart');
59
+
60
+ const result = await page.evaluate(buildPlaceOrderEvaluate()).catch((error) => {
61
+ throw new CommandExecutionError(`blinkit place-order failed: ${error?.message || error}`);
62
+ });
63
+ if (!result?.status) throw new CommandExecutionError('blinkit place-order returned no status');
64
+ return [{
65
+ status: result.status,
66
+ confirmed: true,
67
+ itemCount: summary.itemCount,
68
+ payable: summary.payable,
69
+ orderId: result.orderId || '',
70
+ url: result.url || '',
71
+ message: result.message || '',
72
+ }];
73
+ },
74
+ });
75
+
76
+ export const __test__ = {
77
+ buildPlaceOrderEvaluate,
78
+ };
@@ -0,0 +1,63 @@
1
+ import { CommandExecutionError, EmptyResultError } from '@agentrhq/webcmd/errors';
2
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
3
+ import { DOMAIN, normalizeProductSnippet, requireProductId, resolveCoordinates } from './utils.js';
4
+
5
+ function buildProductEvaluate(productId, lat, lon) {
6
+ return `
7
+ (async () => {
8
+ const headers = { lat: ${JSON.stringify(lat)}, lon: ${JSON.stringify(lon)}, app_client: 'consumer_web' };
9
+ const resp = await fetch('/v1/layout/product/' + ${JSON.stringify(productId)}, { method: 'POST', credentials: 'include', headers });
10
+ const raw = await resp.text();
11
+ let json;
12
+ try { json = JSON.parse(raw); } catch { return { ok: false, status: resp.status, error: raw.slice(0, 200) }; }
13
+ if (!resp.ok || json.error) return { ok: false, status: resp.status, error: json.error || json.message || raw.slice(0, 200) };
14
+ return { ok: true, snippets: json?.response?.snippets || [] };
15
+ })()
16
+ `;
17
+ }
18
+
19
+ function normalizeProduct(snippets, productId) {
20
+ const title = snippets.find((snippet) => snippet?.widget_type === 'text_right_icons_rating_snippet_type');
21
+ const strip = snippets.find((snippet) => snippet?.widget_type === 'product_atc_strip');
22
+ const row = normalizeProductSnippet(strip) ?? normalizeProductSnippet(title);
23
+ if (!row) return null;
24
+ const titleText = title?.data?.title?.text;
25
+ return { ...row, productId, name: titleText || row.name };
26
+ }
27
+
28
+ cli({
29
+ site: 'blinkit',
30
+ name: 'product',
31
+ access: 'read',
32
+ description: 'Read Blinkit product details for a delivery location',
33
+ domain: DOMAIN,
34
+ strategy: Strategy.COOKIE,
35
+ browser: true,
36
+ navigateBefore: false,
37
+ args: [
38
+ { name: 'productId', required: true, positional: true, help: 'Blinkit product id' },
39
+ { name: 'lat', help: 'Delivery latitude (defaults to current Blinkit browser location)' },
40
+ { name: 'lon', help: 'Delivery longitude (defaults to current Blinkit browser location)' },
41
+ ],
42
+ columns: ['productId', 'name', 'brand', 'variant', 'price', 'mrp', 'currency', 'inventory', 'available', 'imageUrl', 'url'],
43
+ func: async (page, kwargs) => {
44
+ const productId = requireProductId(kwargs.productId);
45
+ await page.goto(`https://blinkit.com/prn/x/prid/${productId}`).catch((error) => {
46
+ throw new CommandExecutionError(`blinkit product navigation failed: ${error?.message || error}`);
47
+ });
48
+ const { lat, lon } = await resolveCoordinates(page, kwargs);
49
+ const result = await page.evaluate(buildProductEvaluate(productId, lat, lon)).catch((error) => {
50
+ throw new CommandExecutionError(`blinkit product request failed: ${error?.message || error}`);
51
+ });
52
+ if (!result?.ok) {
53
+ throw new CommandExecutionError(`blinkit product failed: HTTP ${result?.status ?? 'unknown'} ${result?.error ?? ''}`.trim());
54
+ }
55
+ const row = normalizeProduct(result.snippets ?? [], productId);
56
+ if (!row) throw new EmptyResultError('blinkit product', `No product data for ${productId}`);
57
+ return [row];
58
+ },
59
+ });
60
+
61
+ export const __test__ = {
62
+ normalizeProduct,
63
+ };
@@ -0,0 +1,89 @@
1
+ import { ArgumentError, CommandExecutionError, EmptyResultError } from '@agentrhq/webcmd/errors';
2
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
3
+ import { BASE, MAX_LIMIT, normalizeProductSnippet, parseCoordinate, parseLimit, parsePrice, resolveCoordinates } from './utils.js';
4
+
5
+ function buildSearchEvaluate(query, limit, lat, lon) {
6
+ return `
7
+ (async () => {
8
+ const query = ${JSON.stringify(query)};
9
+ const limit = ${limit};
10
+ const headers = {
11
+ lat: ${JSON.stringify(lat)},
12
+ lon: ${JSON.stringify(lon)},
13
+ app_client: 'consumer_web'
14
+ };
15
+ const rows = [];
16
+ let url = '/v1/layout/search?q=' + encodeURIComponent(query) + '&search_type=type_to_search';
17
+
18
+ for (let page = 0; url && rows.length < limit && page < 8; page += 1) {
19
+ const resp = await fetch(url, { method: 'POST', credentials: 'include', headers });
20
+ const raw = await resp.text();
21
+ let json;
22
+ try {
23
+ json = JSON.parse(raw);
24
+ } catch {
25
+ return { ok: false, status: resp.status, error: raw.slice(0, 200) };
26
+ }
27
+ if (!resp.ok || json.error) {
28
+ return { ok: false, status: resp.status, error: json.error || json.message || raw.slice(0, 200) };
29
+ }
30
+ const snippets = Array.isArray(json?.response?.snippets) ? json.response.snippets : [];
31
+ rows.push(...snippets.filter((snippet) => snippet?.widget_type === 'product_card_snippet_type_2'));
32
+ url = json?.response?.pagination?.next_url || '';
33
+ }
34
+
35
+ return { ok: true, rows: rows.slice(0, limit) };
36
+ })()
37
+ `;
38
+ }
39
+
40
+ cli({
41
+ site: 'blinkit',
42
+ name: 'search',
43
+ access: 'read',
44
+ description: 'Search Blinkit products for a delivery location',
45
+ domain: 'blinkit.com',
46
+ strategy: Strategy.COOKIE,
47
+ browser: true,
48
+ navigateBefore: false,
49
+ args: [
50
+ { name: 'query', required: true, positional: true, help: 'Search keyword' },
51
+ { name: 'limit', type: 'int', default: 20, help: `Max results (max ${MAX_LIMIT})` },
52
+ { name: 'lat', help: 'Delivery latitude (defaults to current Blinkit browser location)' },
53
+ { name: 'lon', help: 'Delivery longitude (defaults to current Blinkit browser location)' },
54
+ ],
55
+ columns: ['rank', 'productId', 'name', 'brand', 'variant', 'price', 'mrp', 'currency', 'inventory', 'available', 'imageUrl', 'url'],
56
+ func: async (page, kwargs) => {
57
+ const query = String(kwargs.query ?? '').trim();
58
+ if (!query) throw new ArgumentError('query cannot be empty');
59
+
60
+ const limit = parseLimit(kwargs.limit);
61
+ const pageUrl = `${BASE}/s/?q=${encodeURIComponent(query)}`;
62
+ await page.goto(pageUrl).catch((error) => {
63
+ throw new CommandExecutionError(`blinkit search navigation failed: ${error?.message || error}`);
64
+ });
65
+ const { lat, lon } = await resolveCoordinates(page, kwargs);
66
+
67
+ const result = await page.evaluate(buildSearchEvaluate(query, limit, lat, lon)).catch((error) => {
68
+ throw new CommandExecutionError(`blinkit search request failed: ${error?.message || error}`);
69
+ });
70
+ if (!result?.ok) {
71
+ throw new CommandExecutionError(`blinkit search failed: HTTP ${result?.status ?? 'unknown'} ${result?.error ?? ''}`.trim());
72
+ }
73
+
74
+ const rows = (result.rows ?? [])
75
+ .map((snippet, index) => normalizeProductSnippet(snippet, index + 1))
76
+ .filter(Boolean);
77
+ if (!rows.length) {
78
+ throw new EmptyResultError('blinkit search', `No products matched "${query}" at ${lat},${lon}`);
79
+ }
80
+ return rows;
81
+ },
82
+ });
83
+
84
+ export const __test__ = {
85
+ normalizeSnippet: normalizeProductSnippet,
86
+ parseCoordinate,
87
+ parseLimit,
88
+ parsePrice,
89
+ };
@@ -0,0 +1,223 @@
1
+ import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@agentrhq/webcmd/errors';
2
+
3
+ export const BASE = 'https://blinkit.com';
4
+ export const DOMAIN = 'blinkit.com';
5
+ export const DEFAULT_LAT = '28.413333';
6
+ export const DEFAULT_LON = '77.072833';
7
+ export const MAX_LIMIT = 48;
8
+
9
+ export function parseLimit(raw) {
10
+ if (raw === undefined || raw === null || raw === '') return 20;
11
+ const limit = Number(raw);
12
+ if (!Number.isInteger(limit) || limit < 1 || limit > MAX_LIMIT) {
13
+ throw new ArgumentError(`--limit must be an integer between 1 and ${MAX_LIMIT}`);
14
+ }
15
+ return limit;
16
+ }
17
+
18
+ export function parseQuantity(raw) {
19
+ const quantity = raw === undefined || raw === null || raw === '' ? 1 : Number(raw);
20
+ if (!Number.isInteger(quantity) || quantity < 1 || quantity > 12) {
21
+ throw new ArgumentError('--quantity must be an integer between 1 and 12');
22
+ }
23
+ return quantity;
24
+ }
25
+
26
+ export function parseCoordinate(raw, label, fallback) {
27
+ const value = raw === undefined || raw === null || raw === '' ? fallback : String(raw);
28
+ const n = Number(value);
29
+ const max = label === 'lat' ? 90 : 180;
30
+ if (!Number.isFinite(n) || n < -max || n > max) {
31
+ throw new ArgumentError(`--${label} must be a number between ${-max} and ${max}`);
32
+ }
33
+ return String(n);
34
+ }
35
+
36
+ export async function resolveCoordinates(page, kwargs = {}) {
37
+ const explicitLat = kwargs.lat !== undefined && kwargs.lat !== null && kwargs.lat !== '';
38
+ const explicitLon = kwargs.lon !== undefined && kwargs.lon !== null && kwargs.lon !== '';
39
+ const location = explicitLat && explicitLon ? null : await page.evaluate(`
40
+ (() => {
41
+ const readJson = (key) => {
42
+ try { return JSON.parse(localStorage.getItem(key) || 'null'); } catch { return null; }
43
+ };
44
+ const coords = window.__reduxStore__?.getState?.()?.data?.location?.coords || readJson('location')?.coords || {};
45
+ return { lat: coords.lat, lon: coords.lon };
46
+ })()
47
+ `).catch(() => null);
48
+ return {
49
+ lat: parseCoordinate(explicitLat ? kwargs.lat : location?.lat, 'lat', DEFAULT_LAT),
50
+ lon: parseCoordinate(explicitLon ? kwargs.lon : location?.lon, 'lon', DEFAULT_LON),
51
+ };
52
+ }
53
+
54
+ export function normalizeLocationState(location = {}) {
55
+ const coords = location.coords || {};
56
+ return {
57
+ selected: Boolean(location.locality || location.area || location.city || location.pinCode || location.pincode || coords.lat || coords.lon),
58
+ label: String(location.label || location.name || location.type || '').trim(),
59
+ area: String(location.locality || location.area || location.landmark || '').trim(),
60
+ city: String(location.city || location.cityName || '').trim(),
61
+ pincode: String(location.pinCode || location.pincode || location.pin || '').trim(),
62
+ hasCoordinates: Boolean(coords.lat || coords.lon),
63
+ source: 'browser',
64
+ };
65
+ }
66
+
67
+ export function requireProductId(raw) {
68
+ const value = String(raw ?? '').trim();
69
+ const match = value.match(/(?:prid\/)?(\d{3,})$/) || value.match(/\/prid\/(\d{3,})/);
70
+ if (!match) throw new ArgumentError('productId must be a Blinkit product id, for example 19512');
71
+ return match[1];
72
+ }
73
+
74
+ export function parsePrice(text) {
75
+ const match = String(text ?? '').replace(/,/g, '').match(/(\d+(?:\.\d+)?)/);
76
+ return match ? Number(match[1]) : null;
77
+ }
78
+
79
+ export function text(node) {
80
+ return String(node?.text ?? '').trim();
81
+ }
82
+
83
+ export function productUrl(productId) {
84
+ return productId ? `${BASE}/prn/x/prid/${productId}` : '';
85
+ }
86
+
87
+ export function normalizeProductSnippet(snippet, rank = undefined) {
88
+ const data = snippet?.data ?? {};
89
+ const cart = data.atc_action?.add_to_cart?.cart_item
90
+ ?? data.stepper_data_v2?.increment_actions?.default?.find((action) => action?.add_to_cart)?.add_to_cart?.cart_item
91
+ ?? data.rfc_actions_v2?.default?.find((action) => action?.remove_from_cart)?.remove_from_cart?.cart_item
92
+ ?? {};
93
+ const productId = String(data.product_id ?? data.meta?.product_id ?? cart.product_id ?? data.identity?.id ?? '').trim();
94
+ const name = text(data.display_name) || text(data.name) || text(data.title) || String(cart.product_name ?? cart.display_name ?? '').trim();
95
+ if (!productId || !name) return null;
96
+
97
+ const price = cart.price ?? parsePrice(text(data.normal_price) || text(data.info_text));
98
+ const mrp = cart.mrp ?? parsePrice(text(data.mrp));
99
+ return {
100
+ ...(rank === undefined ? {} : { rank }),
101
+ productId,
102
+ name,
103
+ brand: text(data.brand_name) || String(cart.brand ?? '').trim(),
104
+ variant: text(data.variant) || String(cart.unit ?? '').trim(),
105
+ price: Number.isFinite(Number(price)) ? Number(price) : null,
106
+ mrp: Number.isFinite(Number(mrp)) ? Number(mrp) : null,
107
+ currency: 'INR',
108
+ inventory: Number.isFinite(Number(data.inventory ?? cart.inventory)) ? Number(data.inventory ?? cart.inventory) : null,
109
+ available: data.is_sold_out === true ? false : data.product_state !== 'unavailable',
110
+ imageUrl: data.image?.url || cart.image_url || '',
111
+ url: productUrl(productId),
112
+ };
113
+ }
114
+
115
+ export function normalizeCartItem(item, fallbackId = '') {
116
+ const product = item?.product ?? item ?? {};
117
+ const productId = String(product.product_id ?? item?.product_id ?? fallbackId ?? '').trim();
118
+ const quantity = Number(item?.quantity ?? product.quantity ?? 0);
119
+ if (!productId || !quantity) return null;
120
+ const price = Number(product.price ?? product.unit_price ?? parsePrice(product.total_price));
121
+ return {
122
+ productId,
123
+ name: String(product.name ?? product.product_name ?? product.display_name ?? '').trim(),
124
+ variant: String(product.unit ?? '').trim(),
125
+ price: Number.isFinite(price) ? price : null,
126
+ quantity,
127
+ total: Number.isFinite(price) ? price * quantity : null,
128
+ inventory: Number.isFinite(Number(product.inventory_limit ?? product.inventory)) ? Number(product.inventory_limit ?? product.inventory) : null,
129
+ merchantId: String(product.merchant_id ?? '').trim(),
130
+ imageUrl: product.image_url || product.image_url_v2 || product.png_image_url || '',
131
+ };
132
+ }
133
+
134
+ export function summarizeCartResponse(response) {
135
+ const cartData = response?.cart_data ?? {};
136
+ const bill = cartData.bill_details ?? {};
137
+ const items = Array.isArray(cartData.items)
138
+ ? cartData.items.map((item) => normalizeCartItem(item)).filter(Boolean)
139
+ : [];
140
+ const storedItems = response?.storedCart?.items && typeof response.storedCart.items === 'object'
141
+ ? Object.entries(response.storedCart.items).map(([id, item]) => normalizeCartItem(item, id)).filter(Boolean)
142
+ : [];
143
+ const finalItems = items.length ? items : storedItems;
144
+ return {
145
+ status: finalItems.length ? 'ok' : 'empty',
146
+ itemCount: Number(bill.total_items ?? response?.storedCart?.count ?? finalItems.reduce((sum, item) => sum + item.quantity, 0) ?? 0),
147
+ itemsTotal: Number(bill.total_cost ?? response?.storedCart?.total ?? 0),
148
+ deliveryCharge: Number(bill.delivery_charge ?? 0),
149
+ handlingCharge: Number(bill.additional_charge ?? 0),
150
+ payable: Number(bill.payable_amount ?? bill.bill_total ?? response?.storedCart?.total ?? 0),
151
+ cartState: response?.cart_state ?? cartData.cart_state ?? response?.storedCart?.cart_state ?? '',
152
+ checkoutBlocked: Boolean(cartData.checkout_block_details),
153
+ validations: (cartData.validations ?? []).map((validation) => validation?.code).filter(Boolean).join(','),
154
+ items: finalItems,
155
+ };
156
+ }
157
+
158
+ export async function gotoBlinkit(page, path = '/') {
159
+ await page.goto(`${BASE}${path}`).catch((error) => {
160
+ throw new CommandExecutionError(`blinkit navigation failed: ${error?.message || error}`);
161
+ });
162
+ }
163
+
164
+ export async function openCartPanel(page) {
165
+ await gotoBlinkit(page, '/');
166
+ await page.wait(1);
167
+ await page.evaluate(`
168
+ (() => {
169
+ const nodes = Array.from(document.querySelectorAll('[role="button"], header div, header button, header a'));
170
+ const target = nodes
171
+ .filter((node) => {
172
+ const value = node.innerText || node.textContent || '';
173
+ return /My Cart/i.test(value) || (/\\d+\\s+items?/i.test(value) && /₹\\s*\\d+/i.test(value));
174
+ })
175
+ .sort((a, b) => (a.innerText || a.textContent || '').length - (b.innerText || b.textContent || '').length)[0];
176
+ target?.dispatchEvent?.(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
177
+ return Boolean(target);
178
+ })()
179
+ `).catch((error) => {
180
+ throw new CommandExecutionError(`blinkit cart open failed: ${error?.message || error}`);
181
+ });
182
+ for (let attempt = 0; attempt < 5; attempt += 1) {
183
+ await page.wait(1);
184
+ const ready = await page.evaluate(`
185
+ (() => Boolean(window.__reduxStore__?.getState?.()?.ui?.cart?.cartScreen?.cartResponse?.cart_data))
186
+ `).catch(() => false);
187
+ if (ready) return;
188
+ }
189
+ }
190
+
191
+ export async function readCartState(page) {
192
+ const result = await page.evaluate(`
193
+ (() => {
194
+ const readJson = (key) => {
195
+ try { return JSON.parse(localStorage.getItem(key) || 'null'); } catch { return null; }
196
+ };
197
+ const state = window.__reduxStore__?.getState?.();
198
+ const response = state?.ui?.cart?.cartScreen?.cartResponse || null;
199
+ return {
200
+ ok: true,
201
+ loggedIn: !/^Login$/m.test(document.body.innerText || '') && Boolean(readJson('auth')?.accessToken || state?.data?.auth?.accessToken),
202
+ response,
203
+ storedCart: state?.data?.cart || readJson('cart') || null,
204
+ checkout: state?.ui?.checkout || readJson('checkout') || null,
205
+ location: state?.data?.location || readJson('location') || null,
206
+ };
207
+ })()
208
+ `).catch((error) => {
209
+ throw new CommandExecutionError(`blinkit cart state read failed: ${error?.message || error}`);
210
+ });
211
+ if (!result?.ok) throw new CommandExecutionError('blinkit cart state read failed');
212
+ return result;
213
+ }
214
+
215
+ export function ensureLoggedIn(state, action = 'Blinkit command') {
216
+ if (!state?.loggedIn) {
217
+ throw new AuthRequiredError(DOMAIN, `${action} requires a logged-in Blinkit browser session. Run webcmd blinkit login first.`);
218
+ }
219
+ }
220
+
221
+ export function ensureCartHasItems(summary) {
222
+ if (!summary.itemCount) throw new EmptyResultError('blinkit cart', 'Cart is empty');
223
+ }