@agentrhq/webcmd 0.2.2 → 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 (52) hide show
  1. package/cli-manifest.json +1269 -115
  2. package/clis/bigbasket/add-to-cart.js +82 -0
  3. package/clis/bigbasket/bigbasket.test.js +255 -0
  4. package/clis/bigbasket/cart.js +81 -0
  5. package/clis/bigbasket/category.js +30 -0
  6. package/clis/bigbasket/checkout.js +71 -0
  7. package/clis/bigbasket/location.js +30 -0
  8. package/clis/bigbasket/product.js +79 -0
  9. package/clis/bigbasket/search.js +30 -0
  10. package/clis/bigbasket/utils.js +207 -0
  11. package/clis/blinkit/add-to-cart.js +123 -0
  12. package/clis/blinkit/auth.js +99 -0
  13. package/clis/blinkit/blinkit.test.js +168 -0
  14. package/clis/blinkit/cart.js +34 -0
  15. package/clis/blinkit/checkout.js +32 -0
  16. package/clis/blinkit/location.js +29 -0
  17. package/clis/blinkit/place-order.js +78 -0
  18. package/clis/blinkit/product.js +63 -0
  19. package/clis/blinkit/search.js +89 -0
  20. package/clis/blinkit/utils.js +223 -0
  21. package/clis/district/checkout.js +71 -1
  22. package/clis/practo/appointment.js +21 -0
  23. package/clis/practo/appointments.js +27 -0
  24. package/clis/practo/book-confirm.js +35 -0
  25. package/clis/practo/book-preview.js +24 -0
  26. package/clis/practo/booking-link.js +24 -0
  27. package/clis/practo/cancel.js +29 -0
  28. package/clis/practo/contact.js +21 -0
  29. package/clis/practo/login.js +21 -0
  30. package/clis/practo/practo.test.js +136 -0
  31. package/clis/practo/profile.js +31 -0
  32. package/clis/practo/search.js +30 -0
  33. package/clis/practo/slots.js +19 -0
  34. package/clis/practo/utils.js +374 -0
  35. package/clis/practo/whoami.js +28 -0
  36. package/clis/zepto/add-to-cart.js +53 -0
  37. package/clis/zepto/auth.js +59 -0
  38. package/clis/zepto/cart.js +23 -0
  39. package/clis/zepto/checkout.js +60 -0
  40. package/clis/zepto/location.js +20 -0
  41. package/clis/zepto/place-order.js +47 -0
  42. package/clis/zepto/product.js +52 -0
  43. package/clis/zepto/search.js +30 -0
  44. package/clis/zepto/utils.js +228 -0
  45. package/clis/zepto/zepto.test.js +238 -0
  46. package/dist/src/generate-release-notes-cli.test.js +55 -1
  47. package/dist/src/release-notes.d.ts +3 -1
  48. package/dist/src/release-notes.js +44 -1
  49. package/dist/src/release-notes.test.js +39 -1
  50. package/package.json +1 -1
  51. package/scripts/generate-release-notes.ts +31 -0
  52. package/skills/webcmd-adapter-author/SKILL.md +9 -0
@@ -0,0 +1,374 @@
1
+ import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@agentrhq/webcmd/errors';
2
+
3
+ const SLOT_TIME_RE = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
4
+ export const PRACTO = 'https://www.practo.com';
5
+ export const DRIVE = 'https://drive.practo.com';
6
+
7
+ export function slugifyPathPart(value, label) {
8
+ const out = String(value ?? '').trim().toLowerCase()
9
+ .replace(/&/g, ' and ')
10
+ .replace(/[^a-z0-9]+/g, '-')
11
+ .replace(/^-+|-+$/g, '');
12
+ if (!out) throw new ArgumentError(`${label} is required`);
13
+ return out;
14
+ }
15
+
16
+ export function normalizeLimit(value, defaultValue = 10, max = 25) {
17
+ const n = Number(value ?? defaultValue);
18
+ if (!Number.isInteger(n) || n < 1 || n > max) {
19
+ throw new ArgumentError(`limit must be an integer from 1 to ${max}`);
20
+ }
21
+ return n;
22
+ }
23
+
24
+ export function normalizePracticeDoctorId(value) {
25
+ const id = String(value ?? '').trim();
26
+ if (!/^\d+$/.test(id)) {
27
+ throw new ArgumentError('practice_doctor_id must be numeric', 'Use `webcmd practo search ...` or `webcmd practo slots <practice_doctor_id>` first.');
28
+ }
29
+ return id;
30
+ }
31
+
32
+ export function normalizeAppointmentId(value) {
33
+ const id = String(value ?? '').trim();
34
+ if (!id) throw new ArgumentError('appointment_id is required');
35
+ return id;
36
+ }
37
+
38
+ export function normalizeSlotTime(value) {
39
+ const raw = String(value ?? '').trim();
40
+ if (!SLOT_TIME_RE.test(raw)) {
41
+ throw new ArgumentError('time must be YYYY-MM-DD HH:mm:ss', 'Example: --time "2026-07-10 10:30:00"');
42
+ }
43
+ return raw;
44
+ }
45
+
46
+ export function normalizeConfirm(value) {
47
+ return value === true || value === 'true';
48
+ }
49
+
50
+ export function unwrapBrowserResult(value) {
51
+ if (value && typeof value === 'object' && Object.prototype.hasOwnProperty.call(value, 'data') && Object.prototype.hasOwnProperty.call(value, 'session')) {
52
+ return value.data;
53
+ }
54
+ return value;
55
+ }
56
+
57
+ export async function evalJson(page, url, init = {}) {
58
+ const result = unwrapBrowserResult(await page.evaluate(`(async () => {
59
+ try {
60
+ const res = await fetch(${JSON.stringify(url)}, {
61
+ credentials: 'include',
62
+ method: ${JSON.stringify(init.method || 'GET')},
63
+ headers: ${JSON.stringify(init.headers || { Accept: 'application/json' })},
64
+ body: ${init.body == null ? 'undefined' : JSON.stringify(init.body)}
65
+ });
66
+ const text = await res.text();
67
+ let data = null;
68
+ try { data = text ? JSON.parse(text) : null; } catch { data = { text }; }
69
+ if (!res.ok) return { __error: 'HTTP ' + res.status, status: res.status, data };
70
+ return data;
71
+ } catch (e) {
72
+ return { __error: String(e && e.message || e) };
73
+ }
74
+ })()`));
75
+ return assertJsonOk(result, url);
76
+ }
77
+
78
+ export async function practoJson(page, pathOrUrl) {
79
+ await page.goto(PRACTO);
80
+ return evalJson(page, pathOrUrl.startsWith('http') ? pathOrUrl : `${PRACTO}${pathOrUrl}`);
81
+ }
82
+
83
+ export async function driveJson(page, path, init = {}) {
84
+ await page.goto(`${DRIVE}/appointments`);
85
+ return evalJson(page, `${DRIVE}${path}`, init);
86
+ }
87
+
88
+ function displayName(user) {
89
+ return String(user?.name || user?.user?.name || user?.profile?.name || user?.logged_in_user?.name || '').trim();
90
+ }
91
+
92
+ export async function probeIdentity(page) {
93
+ let user;
94
+ try {
95
+ user = await practoJson(page, '/logged_in_user');
96
+ } catch (error) {
97
+ if (/HTTP 401|HTTP 403/.test(error?.message || '')) {
98
+ throw new AuthRequiredError('www.practo.com', 'Practo /logged_in_user rejected the current browser session');
99
+ }
100
+ throw error;
101
+ }
102
+ if (!user || user.__error) throw new AuthRequiredError('www.practo.com');
103
+ return [{ logged_in: true, site: 'practo', name: displayName(user) }];
104
+ }
105
+
106
+ export function buildSearchUrl({ city, specialty, locality }) {
107
+ const parts = [
108
+ 'https://www.practo.com',
109
+ slugifyPathPart(city, 'city'),
110
+ slugifyPathPart(specialty, 'specialty'),
111
+ ];
112
+ if (String(locality ?? '').trim()) parts.push(slugifyPathPart(locality, 'locality'));
113
+ return parts.join('/');
114
+ }
115
+
116
+ export function buildSlotsUrl(practiceDoctorId) {
117
+ return `https://www.practo.com/health/api/practicedoctors/${normalizePracticeDoctorId(practiceDoctorId)}/slots?mobile=true&group_by_hour=true&logged_in_api=false&first_available=true&`;
118
+ }
119
+
120
+ export async function rowsForSlots(page, practiceDoctorId) {
121
+ const payload = await practoJson(page, buildSlotsUrl(practiceDoctorId));
122
+ const rows = rowsFromSlotsPayload(payload);
123
+ if (rows.length === 0) throw new EmptyResultError('practo slots', 'No available slots were returned for that practice_doctor_id.');
124
+ return rows;
125
+ }
126
+
127
+ function slugFromProfileUrl(profileUrl) {
128
+ const raw = text(profileUrl);
129
+ if (!raw) return 'doctor';
130
+ try {
131
+ const url = new URL(raw, 'https://www.practo.com');
132
+ const parts = url.pathname.split('/').filter(Boolean);
133
+ const doctorIndex = parts.indexOf('doctor');
134
+ return doctorIndex >= 0 && parts[doctorIndex + 1] ? parts[doctorIndex + 1] : 'doctor';
135
+ } catch {
136
+ return 'doctor';
137
+ }
138
+ }
139
+
140
+ export function buildBookingUrl({ practiceDoctorId, slotTime, appointmentToken = '', amount = '', prepaid = false, profileUrl = '' }) {
141
+ const params = new URLSearchParams();
142
+ params.set('type', 'abs');
143
+ params.set('doctor_id', normalizePracticeDoctorId(practiceDoctorId));
144
+ params.set('appointment_time', normalizeSlotTime(slotTime));
145
+ if (appointmentToken) params.set('appointment_token', String(appointmentToken));
146
+ params.set('prepaid', String(Boolean(prepaid)));
147
+ if (amount !== '' && amount != null) params.set('amount', String(amount));
148
+ return `https://www.practo.com/appointment/${slugFromProfileUrl(profileUrl)}/${practiceDoctorId}/book?${params.toString()}`;
149
+ }
150
+
151
+ export function bookingPreviewFromPageData(data, context) {
152
+ const text = String(data?.text || '');
153
+ const confirmText = data?.confirmText || '';
154
+ const paymentMode = context.paymentMode || data?.paymentMode || (/pay at clinic/i.test(text) ? 'Pay At Clinic' : '');
155
+ return [{
156
+ practice_doctor_id: context.practiceDoctorId,
157
+ time: context.slotTime,
158
+ amount: context.amount,
159
+ prepaid: context.prepaid,
160
+ payment_mode: paymentMode,
161
+ requires_payment: Boolean(paymentMode && !/pay at clinic/i.test(paymentMode)),
162
+ confirm_button: confirmText,
163
+ booking_url: context.url,
164
+ }];
165
+ }
166
+
167
+ export async function prepareBookingPage(page, kwargs) {
168
+ const practiceDoctorId = normalizePracticeDoctorId(kwargs.practice_doctor_id ?? kwargs.practiceDoctorId);
169
+ const slotTime = normalizeSlotTime(kwargs.time);
170
+ const slots = await rowsForSlots(page, practiceDoctorId);
171
+ const slot = findSlot(slots, slotTime);
172
+ let paymentMode = '';
173
+ try {
174
+ const summary = await practoJson(page, `/health/api/appointment/payment/summary?appointment_type=abs&platform=web&practice_doctor_id=${practiceDoctorId}`);
175
+ const modes = summary?.payment_mode_details || summary?.data?.payment_mode_details || [];
176
+ paymentMode = String(modes?.[0]?.mode || modes?.[0]?.payment_mode || '').trim();
177
+ } catch {
178
+ paymentMode = '';
179
+ }
180
+ const url = buildBookingUrl({
181
+ practiceDoctorId,
182
+ slotTime,
183
+ appointmentToken: slot.appointment_token,
184
+ amount: slot.amount,
185
+ prepaid: slot.prepaid,
186
+ profileUrl: kwargs['profile-url'] ?? kwargs.profile_url ?? kwargs.profileUrl,
187
+ });
188
+ await page.goto(url);
189
+ let ready = false;
190
+ for (let i = 0; i < 60; i++) {
191
+ const readyNow = unwrapBrowserResult(await page.evaluate(`(() => /Confirm Clinic Visit|Enter your mobile number|sign in|login/i.test(document.body?.innerText || ''))()`));
192
+ if (readyNow) {
193
+ ready = true;
194
+ break;
195
+ }
196
+ await page.wait(0.5);
197
+ }
198
+ const data = unwrapBrowserResult(await page.evaluate(`(() => {
199
+ const text = document.body?.innerText || '';
200
+ const buttons = Array.from(document.querySelectorAll('button, [role="button"], input[type="submit"]'))
201
+ .map((el) => (el.innerText || el.value || el.textContent || '').replace(/\\s+/g, ' ').trim())
202
+ .filter(Boolean);
203
+ return {
204
+ url: location.href,
205
+ text,
206
+ buttons,
207
+ confirmText: buttons.find((label) => /confirm clinic visit|confirm/i.test(label)) || '',
208
+ paymentMode: (/pay at clinic/i.test(text) && 'Pay At Clinic') || (/pay online/i.test(text) && 'Pay Online') || ''
209
+ };
210
+ })()`));
211
+ if (/enter your mobile number|login|sign in/i.test(String(data?.text || ''))) {
212
+ throw new AuthRequiredError('www.practo.com', 'Booking page requires a logged-in Practo session');
213
+ }
214
+ if (!ready || !data?.confirmText) {
215
+ throw new CommandExecutionError(
216
+ `Practo booking page did not expose a confirm button at ${data?.url || 'unknown URL'}`,
217
+ 'Pass --profile-url from `webcmd practo search ...` so the adapter can build Practo\'s canonical booking URL.',
218
+ );
219
+ }
220
+ return { slot, data, context: { practiceDoctorId, slotTime, amount: slot.amount, prepaid: slot.prepaid, paymentMode, url } };
221
+ }
222
+
223
+ export async function submitBookingPage(page) {
224
+ const result = unwrapBrowserResult(await page.evaluate(`(async () => {
225
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
226
+ const buttons = Array.from(document.querySelectorAll('button, [role="button"], input[type="submit"]'));
227
+ const button = buttons.find((el) => /confirm clinic visit|confirm/i.test(el.innerText || el.value || el.textContent || ''));
228
+ if (!button) return { ok: false, message: 'Confirm Clinic Visit button not found' };
229
+ button.click();
230
+ await sleep(3000);
231
+ return { ok: true, url: location.href, text: document.body?.innerText || '' };
232
+ })()`));
233
+ if (!result?.ok) throw new CommandExecutionError(result?.message || 'Failed to click Practo confirmation button');
234
+ return result;
235
+ }
236
+
237
+ export async function cancelAppointment(page, appointmentId) {
238
+ await page.goto(`${DRIVE}/appointments`);
239
+ const result = unwrapBrowserResult(await page.evaluate(`(async () => {
240
+ const token = decodeURIComponent((document.cookie.split('; ').find((c) => c.startsWith('X-Genesis-Token=')) || '').split('=')[1] || '');
241
+ if (!token) return { ok: false, message: 'X-Genesis-Token cookie missing' };
242
+ const res = await fetch(${JSON.stringify(`${DRIVE}/api/record/v2/cancel_appointment/${appointmentId}`)}, {
243
+ method: 'PATCH',
244
+ credentials: 'include',
245
+ headers: { 'Accept': 'application/json', 'X-Genesis-Token': token }
246
+ });
247
+ const text = await res.text();
248
+ return { ok: res.ok, status: res.status, text };
249
+ })()`));
250
+ if (!result?.ok) {
251
+ if (result?.status === 401 || result?.status === 403) throw new AuthRequiredError('drive.practo.com', `Practo Drive cancel returned HTTP ${result.status}`);
252
+ throw new CommandExecutionError(`Practo cancel failed: ${result?.message || result?.status || 'unknown'}`);
253
+ }
254
+ }
255
+
256
+ function text(value) {
257
+ if (value == null) return '';
258
+ if (typeof value === 'object') {
259
+ return text(firstDefined(value.name, value.practice_name, value.establishment_name, value.clinic_name, value.locality));
260
+ }
261
+ return String(value).replace(/\s+/g, ' ').trim();
262
+ }
263
+
264
+ function firstDefined(...values) {
265
+ return values.find((value) => value !== undefined && value !== null && value !== '');
266
+ }
267
+
268
+ function asAbsPractoUrl(value) {
269
+ const raw = text(value);
270
+ if (!raw) return '';
271
+ try {
272
+ return new URL(raw, 'https://www.practo.com').toString();
273
+ } catch {
274
+ return raw;
275
+ }
276
+ }
277
+
278
+ function doctorsFromState(state) {
279
+ const doctors = state?.listingV2?.doctors?.entities ?? state?.doctors?.entities ?? state?.doctors;
280
+ if (Array.isArray(doctors)) return doctors;
281
+ if (doctors && typeof doctors === 'object') return Object.values(doctors);
282
+ return [];
283
+ }
284
+
285
+ export function rowsFromSearchState(state, limit = 10) {
286
+ return doctorsFromState(state).slice(0, limit).map((doctor, i) => {
287
+ const practiceDoctorId = firstDefined(doctor.practice_doctor_id, doctor.relation_id, doctor.id);
288
+ const name = firstDefined(doctor.doctor_name, doctor.name, doctor.display_name);
289
+ if (!practiceDoctorId || !name) return null;
290
+ return {
291
+ rank: i + 1,
292
+ practice_doctor_id: String(practiceDoctorId),
293
+ doctor_id: text(firstDefined(doctor.doctor_id, doctor.provider_id)),
294
+ practice_id: text(firstDefined(doctor.practice_id, doctor.establishment_id)),
295
+ name: text(name),
296
+ specialty: text(firstDefined(doctor.specialization, doctor.speciality, doctor.specialty)),
297
+ experience_years: firstDefined(doctor.experience_years, doctor.experience),
298
+ locality: text(firstDefined(doctor.locality, doctor.locality_name)),
299
+ city: text(doctor.city),
300
+ clinic: text(firstDefined(doctor.practice, doctor.practice_name, doctor.clinic_name)),
301
+ fee: firstDefined(doctor.consultation_fees, doctor.fees, doctor.fee),
302
+ next_available: text(firstDefined(doctor.next_available_timestamp, doctor.next_available, doctor.first_available_day_slots?.[0]?.time)),
303
+ profile_url: asAbsPractoUrl(firstDefined(doctor.profile_url, doctor.url, doctor.translated_new_slug)),
304
+ };
305
+ }).filter(Boolean);
306
+ }
307
+
308
+ function flattenSlots(value, out = []) {
309
+ if (!value) return out;
310
+ if (Array.isArray(value)) {
311
+ for (const item of value) flattenSlots(item, out);
312
+ return out;
313
+ }
314
+ if (typeof value !== 'object') return out;
315
+ const time = firstDefined(value.ts, value.start_time, value.appointment_time, value.slot_time);
316
+ if (time) out.push(value);
317
+ else if (value.time && !value.timeslots && !value.slots) out.push(value);
318
+ for (const key of ['timeslots', 'slots', 'items', 'available_slots', 'hour_slots']) {
319
+ if (value[key]) flattenSlots(value[key], out);
320
+ }
321
+ return out;
322
+ }
323
+
324
+ export function rowsFromSlotsPayload(payload) {
325
+ const data = payload?.data ?? payload;
326
+ const practiceDoctorId = text(firstDefined(data?.practice_doctor_id, data?.practiceDoctorId));
327
+ const amount = firstDefined(data?.amount, data?.appointment_payment_details?.amount, data?.fee);
328
+ const prepaid = Boolean(firstDefined(data?.prepaid, data?.appointment_payment_details?.prepaid, false));
329
+ const appointmentToken = text(firstDefined(data?.appointment_token, data?.appointmentToken));
330
+ return flattenSlots(data?.slots ?? data).map((slot) => ({
331
+ practice_doctor_id: text(firstDefined(slot.practice_doctor_id, practiceDoctorId)),
332
+ time: text(firstDefined(slot.ts, slot.time, slot.start_time, slot.appointment_time, slot.slot_time)),
333
+ available: firstDefined(slot.available, slot.is_available, true) !== false,
334
+ amount: firstDefined(slot.amount, amount),
335
+ prepaid: Boolean(firstDefined(slot.prepaid, prepaid)),
336
+ appointment_token: text(firstDefined(slot.appointment_token, appointmentToken)),
337
+ })).filter((row) => row.practice_doctor_id && row.time);
338
+ }
339
+
340
+ export function findSlot(slots, slotTime) {
341
+ const wanted = normalizeSlotTime(slotTime);
342
+ const slot = slots.find((row) => row.time === wanted);
343
+ if (!slot) {
344
+ throw new EmptyResultError('practo slots', `No Practo slot matched ${wanted}. Run \`webcmd practo slots <practice_doctor_id>\` first.`);
345
+ }
346
+ if (slot.available === false) {
347
+ throw new EmptyResultError('practo slots', `Practo slot ${wanted} is not available.`);
348
+ }
349
+ return slot;
350
+ }
351
+
352
+ export function assertJsonOk(result, label) {
353
+ if (!result || typeof result !== 'object') {
354
+ throw new CommandExecutionError(`${label} returned no data`);
355
+ }
356
+ if (result.__error) {
357
+ throw new CommandExecutionError(`${label} failed: ${result.__error}`);
358
+ }
359
+ return result;
360
+ }
361
+
362
+ export const __test__ = {
363
+ buildSearchUrl,
364
+ buildSlotsUrl,
365
+ buildBookingUrl,
366
+ normalizeConfirm,
367
+ normalizeLimit,
368
+ normalizePracticeDoctorId,
369
+ normalizeAppointmentId,
370
+ normalizeSlotTime,
371
+ rowsFromSearchState,
372
+ rowsFromSlotsPayload,
373
+ findSlot,
374
+ };
@@ -0,0 +1,28 @@
1
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
2
+ import { probeIdentity } from './utils.js';
3
+
4
+ cli({
5
+ site: 'practo',
6
+ name: 'whoami',
7
+ aliases: ['auth-status'],
8
+ access: 'read',
9
+ description: 'Show whether the current browser session is logged into Practo',
10
+ domain: 'www.practo.com',
11
+ strategy: Strategy.COOKIE,
12
+ browser: true,
13
+ navigateBefore: false,
14
+ siteSession: 'persistent',
15
+ args: [],
16
+ columns: ['logged_in', 'site', 'name'],
17
+ authStatus: {
18
+ quickCheck: async (page) => {
19
+ try {
20
+ const rows = await probeIdentity(page);
21
+ return { logged_in: true, name: rows[0].name };
22
+ } catch {
23
+ return { logged_in: false };
24
+ }
25
+ },
26
+ },
27
+ func: probeIdentity,
28
+ });
@@ -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 };