@agentrhq/webcmd 0.2.2 → 0.2.4
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.
- package/cli-manifest.json +1278 -115
- package/clis/bigbasket/add-to-cart.js +82 -0
- package/clis/bigbasket/bigbasket.test.js +255 -0
- package/clis/bigbasket/cart.js +81 -0
- package/clis/bigbasket/category.js +30 -0
- package/clis/bigbasket/checkout.js +71 -0
- package/clis/bigbasket/location.js +30 -0
- package/clis/bigbasket/product.js +79 -0
- package/clis/bigbasket/search.js +30 -0
- package/clis/bigbasket/utils.js +207 -0
- package/clis/blinkit/add-to-cart.js +123 -0
- package/clis/blinkit/auth.js +99 -0
- package/clis/blinkit/blinkit.test.js +168 -0
- package/clis/blinkit/cart.js +34 -0
- package/clis/blinkit/checkout.js +32 -0
- package/clis/blinkit/location.js +29 -0
- package/clis/blinkit/place-order.js +78 -0
- package/clis/blinkit/product.js +63 -0
- package/clis/blinkit/search.js +89 -0
- package/clis/blinkit/utils.js +223 -0
- package/clis/district/checkout.js +71 -1
- package/clis/practo/appointment.js +21 -0
- package/clis/practo/appointments.js +27 -0
- package/clis/practo/book-confirm.js +35 -0
- package/clis/practo/book-preview.js +24 -0
- package/clis/practo/booking-link.js +24 -0
- package/clis/practo/cancel.js +29 -0
- package/clis/practo/contact.js +21 -0
- package/clis/practo/login.js +48 -0
- package/clis/practo/practo.test.js +154 -0
- package/clis/practo/profile.js +31 -0
- package/clis/practo/search.js +30 -0
- package/clis/practo/slots.js +19 -0
- package/clis/practo/utils.js +374 -0
- package/clis/practo/whoami.js +28 -0
- package/clis/zepto/add-to-cart.js +53 -0
- package/clis/zepto/auth.js +59 -0
- package/clis/zepto/cart.js +23 -0
- package/clis/zepto/checkout.js +60 -0
- package/clis/zepto/location.js +20 -0
- package/clis/zepto/place-order.js +47 -0
- package/clis/zepto/product.js +52 -0
- package/clis/zepto/search.js +30 -0
- package/clis/zepto/utils.js +228 -0
- package/clis/zepto/zepto.test.js +238 -0
- package/dist/src/browser/daemon-lifecycle.d.ts +2 -0
- package/dist/src/browser/daemon-lifecycle.js +28 -6
- package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +5 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.js +116 -2
- package/dist/src/browser/runtime/local-cloak/session-manager.test.js +36 -0
- package/dist/src/browser.test.js +8 -4
- package/dist/src/cli.js +96 -0
- package/dist/src/cli.test.js +2 -2
- package/dist/src/generate-release-notes-cli.test.js +87 -13
- package/dist/src/plugin-catalog.d.ts +46 -0
- package/dist/src/plugin-catalog.js +178 -0
- package/dist/src/plugin-catalog.test.d.ts +1 -0
- package/dist/src/plugin-catalog.test.js +95 -0
- package/dist/src/release-notes.d.ts +4 -2
- package/dist/src/release-notes.js +67 -20
- package/dist/src/release-notes.test.js +67 -9
- package/package.json +2 -1
- package/plugin-catalog.json +10 -0
- package/scripts/generate-release-notes.ts +33 -4
- package/skills/webcmd-adapter-author/SKILL.md +9 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { cli, Strategy } from '@agentrhq/webcmd/registry';
|
|
2
|
+
import { bookingPreviewFromPageData, prepareBookingPage } from './utils.js';
|
|
3
|
+
|
|
4
|
+
cli({
|
|
5
|
+
site: 'practo',
|
|
6
|
+
name: 'book-preview',
|
|
7
|
+
access: 'read',
|
|
8
|
+
description: 'Preview Practo booking details for a selected slot without confirming',
|
|
9
|
+
domain: 'www.practo.com',
|
|
10
|
+
strategy: Strategy.COOKIE,
|
|
11
|
+
browser: true,
|
|
12
|
+
navigateBefore: false,
|
|
13
|
+
siteSession: 'persistent',
|
|
14
|
+
args: [
|
|
15
|
+
{ name: 'practice_doctor_id', positional: true, required: true, help: 'Practo practice_doctor_id' },
|
|
16
|
+
{ name: 'time', required: true, help: 'Slot time YYYY-MM-DD HH:mm:ss' },
|
|
17
|
+
{ name: 'profile-url', required: false, help: 'Doctor profile_url from `practo search`, used to build a canonical booking URL' },
|
|
18
|
+
],
|
|
19
|
+
columns: ['practice_doctor_id', 'time', 'amount', 'prepaid', 'payment_mode', 'requires_payment', 'confirm_button', 'booking_url'],
|
|
20
|
+
func: async (page, kwargs) => {
|
|
21
|
+
const prepared = await prepareBookingPage(page, kwargs);
|
|
22
|
+
return bookingPreviewFromPageData(prepared.data, prepared.context);
|
|
23
|
+
},
|
|
24
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { cli, Strategy } from '@agentrhq/webcmd/registry';
|
|
2
|
+
import { buildBookingUrl, findSlot, normalizePracticeDoctorId, rowsForSlots } from './utils.js';
|
|
3
|
+
|
|
4
|
+
cli({
|
|
5
|
+
site: 'practo',
|
|
6
|
+
name: 'booking-link',
|
|
7
|
+
access: 'read',
|
|
8
|
+
description: 'Build a Practo booking URL for a selected slot without confirming it',
|
|
9
|
+
domain: 'www.practo.com',
|
|
10
|
+
strategy: Strategy.COOKIE,
|
|
11
|
+
browser: true,
|
|
12
|
+
navigateBefore: false,
|
|
13
|
+
args: [
|
|
14
|
+
{ name: 'practice_doctor_id', positional: true, required: true, help: 'Practo practice_doctor_id' },
|
|
15
|
+
{ name: 'time', required: true, help: 'Slot time YYYY-MM-DD HH:mm:ss' },
|
|
16
|
+
{ name: 'profile-url', required: false, help: 'Doctor profile_url from `practo search`, used to build a canonical booking URL' },
|
|
17
|
+
],
|
|
18
|
+
columns: ['practice_doctor_id', 'time', 'booking_url'],
|
|
19
|
+
func: async (page, kwargs) => {
|
|
20
|
+
const practiceDoctorId = normalizePracticeDoctorId(kwargs.practice_doctor_id);
|
|
21
|
+
const slot = findSlot(await rowsForSlots(page, practiceDoctorId), kwargs.time);
|
|
22
|
+
return [{ practice_doctor_id: practiceDoctorId, time: slot.time, booking_url: buildBookingUrl({ practiceDoctorId, slotTime: slot.time, appointmentToken: slot.appointment_token, amount: slot.amount, prepaid: slot.prepaid, profileUrl: kwargs['profile-url'] }) }];
|
|
23
|
+
},
|
|
24
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ArgumentError } from '@agentrhq/webcmd/errors';
|
|
2
|
+
import { cli, Strategy } from '@agentrhq/webcmd/registry';
|
|
3
|
+
import { cancelAppointment, normalizeAppointmentId, normalizeConfirm } from './utils.js';
|
|
4
|
+
|
|
5
|
+
cli({
|
|
6
|
+
site: 'practo',
|
|
7
|
+
name: 'cancel',
|
|
8
|
+
access: 'write',
|
|
9
|
+
description: 'Cancel a logged-in Practo Drive appointment after explicit confirmation',
|
|
10
|
+
domain: 'drive.practo.com',
|
|
11
|
+
strategy: Strategy.COOKIE,
|
|
12
|
+
browser: true,
|
|
13
|
+
navigateBefore: false,
|
|
14
|
+
defaultWindowMode: 'foreground',
|
|
15
|
+
siteSession: 'persistent',
|
|
16
|
+
args: [
|
|
17
|
+
{ name: 'appointment_id', positional: true, required: true, help: 'Appointment id from `practo appointments`' },
|
|
18
|
+
{ name: 'confirm', type: 'boolean', default: false, help: 'Required. Set --confirm true to cancel the appointment.' },
|
|
19
|
+
],
|
|
20
|
+
columns: ['status', 'appointment_id'],
|
|
21
|
+
func: async (page, kwargs) => {
|
|
22
|
+
const id = normalizeAppointmentId(kwargs.appointment_id);
|
|
23
|
+
if (!normalizeConfirm(kwargs.confirm)) {
|
|
24
|
+
throw new ArgumentError('Refusing to cancel appointment without --confirm true', 'Example: webcmd practo cancel <appointment_id> --confirm true');
|
|
25
|
+
}
|
|
26
|
+
await cancelAppointment(page, id);
|
|
27
|
+
return [{ status: 'cancelled', appointment_id: id }];
|
|
28
|
+
},
|
|
29
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { cli, Strategy } from '@agentrhq/webcmd/registry';
|
|
2
|
+
import { normalizePracticeDoctorId, practoJson } from './utils.js';
|
|
3
|
+
|
|
4
|
+
cli({
|
|
5
|
+
site: 'practo',
|
|
6
|
+
name: 'contact',
|
|
7
|
+
access: 'read',
|
|
8
|
+
description: 'Get Practo virtual contact number for a practice_doctor_id',
|
|
9
|
+
domain: 'www.practo.com',
|
|
10
|
+
strategy: Strategy.COOKIE,
|
|
11
|
+
browser: true,
|
|
12
|
+
navigateBefore: false,
|
|
13
|
+
args: [{ name: 'practice_doctor_id', positional: true, required: true, help: 'Practo practice_doctor_id from search results' }],
|
|
14
|
+
columns: ['practice_doctor_id', 'phone', 'raw'],
|
|
15
|
+
func: async (page, kwargs) => {
|
|
16
|
+
const id = normalizePracticeDoctorId(kwargs.practice_doctor_id);
|
|
17
|
+
const data = await practoJson(page, `/health/api/vn/vnpractice?practice_doctor_id=${id}`);
|
|
18
|
+
const phone = data?.phone_number || data?.phone || data?.number || data?.data?.phone_number || '';
|
|
19
|
+
return [{ practice_doctor_id: id, phone, raw: phone ? '' : JSON.stringify(data).slice(0, 500) }];
|
|
20
|
+
},
|
|
21
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { AuthRequiredError, TimeoutError } from '@agentrhq/webcmd/errors';
|
|
2
|
+
import { cli, Strategy } from '@agentrhq/webcmd/registry';
|
|
3
|
+
import { PRACTO, probeIdentity } from './utils.js';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_TIMEOUT_SECONDS = 300;
|
|
6
|
+
|
|
7
|
+
function isAuthRequired(error) {
|
|
8
|
+
return error instanceof AuthRequiredError;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
cli({
|
|
12
|
+
site: 'practo',
|
|
13
|
+
name: 'login',
|
|
14
|
+
access: 'write',
|
|
15
|
+
description: 'Open Practo login and wait until the browser session is authenticated',
|
|
16
|
+
domain: 'www.practo.com',
|
|
17
|
+
strategy: Strategy.COOKIE,
|
|
18
|
+
browser: true,
|
|
19
|
+
navigateBefore: false,
|
|
20
|
+
defaultWindowMode: 'foreground',
|
|
21
|
+
siteSession: 'persistent',
|
|
22
|
+
args: [
|
|
23
|
+
{ name: 'timeout', type: 'int', default: DEFAULT_TIMEOUT_SECONDS, help: 'Maximum seconds to wait for the user to finish login' },
|
|
24
|
+
],
|
|
25
|
+
columns: ['status', 'logged_in', 'site', 'name'],
|
|
26
|
+
func: async (page, kwargs) => {
|
|
27
|
+
try {
|
|
28
|
+
const rows = await probeIdentity(page);
|
|
29
|
+
return [{ status: 'already_logged_in', ...rows[0] }];
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (!isAuthRequired(error)) throw error;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
await page.goto(`${PRACTO}/login`, { waitUntil: 'none', settleMs: 1500 });
|
|
35
|
+
const timeout = Number(kwargs.timeout ?? DEFAULT_TIMEOUT_SECONDS);
|
|
36
|
+
const deadline = Date.now() + timeout * 1000;
|
|
37
|
+
while (Date.now() < deadline) {
|
|
38
|
+
await page.wait(2);
|
|
39
|
+
try {
|
|
40
|
+
const rows = await probeIdentity(page);
|
|
41
|
+
return [{ status: 'login_complete', ...rows[0] }];
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (!isAuthRequired(error)) throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
throw new TimeoutError('practo login', timeout, 'Complete Practo login in the browser, then retry.');
|
|
47
|
+
},
|
|
48
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { ArgumentError } from '@agentrhq/webcmd/errors';
|
|
3
|
+
import { getRegistry } from '@agentrhq/webcmd/registry';
|
|
4
|
+
import './appointment.js';
|
|
5
|
+
import './appointments.js';
|
|
6
|
+
import './book-confirm.js';
|
|
7
|
+
import './book-preview.js';
|
|
8
|
+
import './booking-link.js';
|
|
9
|
+
import './cancel.js';
|
|
10
|
+
import './contact.js';
|
|
11
|
+
import './login.js';
|
|
12
|
+
import './profile.js';
|
|
13
|
+
import './search.js';
|
|
14
|
+
import './slots.js';
|
|
15
|
+
import './whoami.js';
|
|
16
|
+
import { __test__ } from './utils.js';
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
buildSearchUrl,
|
|
20
|
+
buildBookingUrl,
|
|
21
|
+
normalizeConfirm,
|
|
22
|
+
normalizeLimit,
|
|
23
|
+
normalizePracticeDoctorId,
|
|
24
|
+
normalizeSlotTime,
|
|
25
|
+
rowsFromSearchState,
|
|
26
|
+
rowsFromSlotsPayload,
|
|
27
|
+
} = __test__;
|
|
28
|
+
|
|
29
|
+
describe('practo helpers', () => {
|
|
30
|
+
it('builds city/specialty search URLs', () => {
|
|
31
|
+
expect(buildSearchUrl({ city: 'Bangalore', specialty: 'Orthopedist' })).toBe('https://www.practo.com/bangalore/orthopedist');
|
|
32
|
+
expect(buildSearchUrl({ city: 'New Delhi', specialty: 'Dentist', locality: 'South Extension' })).toBe('https://www.practo.com/new-delhi/dentist/south-extension');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('validates identity, limits, slot times, and confirmations', () => {
|
|
36
|
+
expect(normalizePracticeDoctorId('859054')).toBe('859054');
|
|
37
|
+
expect(normalizeLimit('3')).toBe(3);
|
|
38
|
+
expect(normalizeSlotTime('2026-07-10 10:30:00')).toBe('2026-07-10 10:30:00');
|
|
39
|
+
expect(normalizeConfirm(true)).toBe(true);
|
|
40
|
+
expect(normalizeConfirm('true')).toBe(true);
|
|
41
|
+
expect(() => normalizePracticeDoctorId('abc')).toThrow(ArgumentError);
|
|
42
|
+
expect(() => normalizeLimit(0)).toThrow(ArgumentError);
|
|
43
|
+
expect(() => normalizeSlotTime('tomorrow')).toThrow(ArgumentError);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('maps listing redux doctors into stable search rows', () => {
|
|
47
|
+
const rows = rowsFromSearchState({
|
|
48
|
+
listingV2: {
|
|
49
|
+
doctors: {
|
|
50
|
+
entities: {
|
|
51
|
+
1: {
|
|
52
|
+
id: 859054,
|
|
53
|
+
doctor_id: 1056089,
|
|
54
|
+
practice_id: 776696,
|
|
55
|
+
doctor_name: 'Dr. Nithin Patel',
|
|
56
|
+
specialization: 'Orthopedist',
|
|
57
|
+
experience_years: 12,
|
|
58
|
+
locality: 'Koramangala',
|
|
59
|
+
city: 'Bangalore',
|
|
60
|
+
practice: { name: 'Practo Clinic' },
|
|
61
|
+
consultation_fees: 1000,
|
|
62
|
+
profile_url: '/bangalore/doctor/dr-nithin-patel-orthopedist',
|
|
63
|
+
next_available_timestamp: '2026-07-10T10:30:00+05:30',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
}, 1);
|
|
69
|
+
expect(rows).toEqual([{
|
|
70
|
+
rank: 1,
|
|
71
|
+
practice_doctor_id: '859054',
|
|
72
|
+
doctor_id: '1056089',
|
|
73
|
+
practice_id: '776696',
|
|
74
|
+
name: 'Dr. Nithin Patel',
|
|
75
|
+
specialty: 'Orthopedist',
|
|
76
|
+
experience_years: 12,
|
|
77
|
+
locality: 'Koramangala',
|
|
78
|
+
city: 'Bangalore',
|
|
79
|
+
clinic: 'Practo Clinic',
|
|
80
|
+
fee: 1000,
|
|
81
|
+
next_available: '2026-07-10T10:30:00+05:30',
|
|
82
|
+
profile_url: 'https://www.practo.com/bangalore/doctor/dr-nithin-patel-orthopedist',
|
|
83
|
+
}]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('maps slot payloads into rows and booking URLs', () => {
|
|
87
|
+
const rows = rowsFromSlotsPayload({
|
|
88
|
+
practice_doctor_id: 859054,
|
|
89
|
+
amount: 1000,
|
|
90
|
+
prepaid: false,
|
|
91
|
+
appointment_token: 'abc',
|
|
92
|
+
slots: [{
|
|
93
|
+
datestamp: '2026-07-10',
|
|
94
|
+
slots: [{ time: '10:00', timeslots: [{ ts: '2026-07-10 10:30:00', available: true }] }],
|
|
95
|
+
}],
|
|
96
|
+
});
|
|
97
|
+
expect(rows).toEqual([{
|
|
98
|
+
practice_doctor_id: '859054',
|
|
99
|
+
time: '2026-07-10 10:30:00',
|
|
100
|
+
available: true,
|
|
101
|
+
amount: 1000,
|
|
102
|
+
prepaid: false,
|
|
103
|
+
appointment_token: 'abc',
|
|
104
|
+
}]);
|
|
105
|
+
expect(buildBookingUrl({ practiceDoctorId: '859054', slotTime: rows[0].time, appointmentToken: 'abc', amount: 1000, prepaid: false }))
|
|
106
|
+
.toContain('/appointment/doctor/859054/book?');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('practo command registry', () => {
|
|
111
|
+
it('registers the approved command surface', () => {
|
|
112
|
+
for (const name of ['whoami', 'login', 'search', 'profile', 'slots', 'contact', 'booking-link', 'appointments', 'appointment', 'book-preview', 'book-confirm', 'cancel']) {
|
|
113
|
+
expect(getRegistry().get(`practo/${name}`)).toBeDefined();
|
|
114
|
+
}
|
|
115
|
+
expect(getRegistry().get('practo/auth-status')).toBe(getRegistry().get('practo/whoami'));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('marks writes as explicit confirm commands', () => {
|
|
119
|
+
const book = getRegistry().get('practo/book-confirm');
|
|
120
|
+
const cancel = getRegistry().get('practo/cancel');
|
|
121
|
+
expect(book.access).toBe('write');
|
|
122
|
+
expect(cancel.access).toBe('write');
|
|
123
|
+
expect(book.args.find((arg) => arg.name === 'confirm')?.type).toBe('boolean');
|
|
124
|
+
expect(cancel.args.find((arg) => arg.name === 'confirm')?.type).toBe('boolean');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('waits for manual login to complete', async () => {
|
|
128
|
+
const login = getRegistry().get('practo/login');
|
|
129
|
+
const page = {
|
|
130
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
131
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
132
|
+
evaluate: vi.fn()
|
|
133
|
+
.mockResolvedValueOnce({ __error: 'HTTP 401', status: 401 })
|
|
134
|
+
.mockResolvedValueOnce({ name: 'Ada' }),
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
await expect(login.func(page, { timeout: 1 })).resolves.toEqual([{
|
|
138
|
+
status: 'login_complete',
|
|
139
|
+
logged_in: true,
|
|
140
|
+
site: 'practo',
|
|
141
|
+
name: 'Ada',
|
|
142
|
+
}]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('refuses real booking without --confirm true', async () => {
|
|
146
|
+
const book = getRegistry().get('practo/book-confirm');
|
|
147
|
+
await expect(book.func({}, { practice_doctor_id: '859054', time: '2026-07-10 10:30:00' })).rejects.toThrow(ArgumentError);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('refuses cancellation without --confirm true', async () => {
|
|
151
|
+
const cancel = getRegistry().get('practo/cancel');
|
|
152
|
+
await expect(cancel.func({}, { appointment_id: 'abc' })).rejects.toThrow(ArgumentError);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { EmptyResultError } from '@agentrhq/webcmd/errors';
|
|
2
|
+
import { cli, Strategy } from '@agentrhq/webcmd/registry';
|
|
3
|
+
import { PRACTO, unwrapBrowserResult } from './utils.js';
|
|
4
|
+
|
|
5
|
+
cli({
|
|
6
|
+
site: 'practo',
|
|
7
|
+
name: 'profile',
|
|
8
|
+
access: 'read',
|
|
9
|
+
description: 'Read public details from a Practo doctor profile URL',
|
|
10
|
+
domain: 'www.practo.com',
|
|
11
|
+
strategy: Strategy.COOKIE,
|
|
12
|
+
browser: true,
|
|
13
|
+
navigateBefore: false,
|
|
14
|
+
args: [{ name: 'url', positional: true, required: true, help: 'Practo doctor profile URL' }],
|
|
15
|
+
columns: ['name', 'specialty', 'experience', 'fee', 'profile_url'],
|
|
16
|
+
func: async (page, kwargs) => {
|
|
17
|
+
const url = new URL(String(kwargs.url), PRACTO).toString();
|
|
18
|
+
await page.goto(url);
|
|
19
|
+
await page.wait(2);
|
|
20
|
+
const row = unwrapBrowserResult(await page.evaluate(`(() => {
|
|
21
|
+
const text = document.body?.innerText || '';
|
|
22
|
+
const title = document.querySelector('h1')?.textContent?.trim() || document.title.replace(/ - Practo.*/, '').trim();
|
|
23
|
+
const specialty = (text.match(/(Orthopedist|Dentist|Dermatologist|Gynecologist|Pediatrician|General Physician|Cardiologist)/i) || [,''])[1];
|
|
24
|
+
const experience = (text.match(/(\\d+\\s+Years? Experience)/i) || [,''])[1];
|
|
25
|
+
const fee = (text.match(/(?:₹|Rs\\.?\\s*)([0-9,]+)/i) || [,''])[1];
|
|
26
|
+
return { name: title, specialty, experience, fee: fee ? Number(fee.replace(/,/g, '')) : '', profile_url: location.href };
|
|
27
|
+
})()`));
|
|
28
|
+
if (!row?.name) throw new EmptyResultError('practo profile');
|
|
29
|
+
return [row];
|
|
30
|
+
},
|
|
31
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { EmptyResultError } from '@agentrhq/webcmd/errors';
|
|
2
|
+
import { cli, Strategy } from '@agentrhq/webcmd/registry';
|
|
3
|
+
import { buildSearchUrl, normalizeLimit, rowsFromSearchState, unwrapBrowserResult } from './utils.js';
|
|
4
|
+
|
|
5
|
+
cli({
|
|
6
|
+
site: 'practo',
|
|
7
|
+
name: 'search',
|
|
8
|
+
access: 'read',
|
|
9
|
+
description: 'Search Practo doctors by specialty, city, and optional locality',
|
|
10
|
+
domain: 'www.practo.com',
|
|
11
|
+
strategy: Strategy.COOKIE,
|
|
12
|
+
browser: true,
|
|
13
|
+
navigateBefore: false,
|
|
14
|
+
args: [
|
|
15
|
+
{ name: 'specialty', positional: true, required: true, help: 'Doctor specialty, e.g. orthopedist or dermatologist' },
|
|
16
|
+
{ name: 'city', default: 'bangalore', help: 'City, e.g. bangalore' },
|
|
17
|
+
{ name: 'locality', help: 'Optional locality, e.g. indiranagar' },
|
|
18
|
+
{ name: 'limit', type: 'int', default: 10, help: 'Max doctors to return (1-25)' },
|
|
19
|
+
],
|
|
20
|
+
columns: ['rank', 'practice_doctor_id', 'doctor_id', 'practice_id', 'name', 'specialty', 'experience_years', 'locality', 'clinic', 'fee', 'next_available', 'profile_url'],
|
|
21
|
+
func: async (page, kwargs) => {
|
|
22
|
+
const limit = normalizeLimit(kwargs.limit);
|
|
23
|
+
await page.goto(buildSearchUrl(kwargs));
|
|
24
|
+
await page.wait(2);
|
|
25
|
+
const state = unwrapBrowserResult(await page.evaluate(`(() => window.__REDUX_STATE__ || window.__PRELOADED_STATE__ || null)()`));
|
|
26
|
+
const rows = rowsFromSearchState(state, limit);
|
|
27
|
+
if (rows.length === 0) throw new EmptyResultError('practo search', 'No doctor cards were found in Practo page state.');
|
|
28
|
+
return rows;
|
|
29
|
+
},
|
|
30
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { cli, Strategy } from '@agentrhq/webcmd/registry';
|
|
2
|
+
import { normalizeLimit, rowsForSlots } from './utils.js';
|
|
3
|
+
|
|
4
|
+
cli({
|
|
5
|
+
site: 'practo',
|
|
6
|
+
name: 'slots',
|
|
7
|
+
access: 'read',
|
|
8
|
+
description: 'List available Practo appointment slots for a practice_doctor_id',
|
|
9
|
+
domain: 'www.practo.com',
|
|
10
|
+
strategy: Strategy.COOKIE,
|
|
11
|
+
browser: true,
|
|
12
|
+
navigateBefore: false,
|
|
13
|
+
args: [
|
|
14
|
+
{ name: 'practice_doctor_id', positional: true, required: true, help: 'Practo practice_doctor_id from search results' },
|
|
15
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Max slots to return (1-25)' },
|
|
16
|
+
],
|
|
17
|
+
columns: ['practice_doctor_id', 'time', 'available', 'amount', 'prepaid', 'appointment_token'],
|
|
18
|
+
func: async (page, kwargs) => (await rowsForSlots(page, kwargs.practice_doctor_id)).filter((row) => row.available).slice(0, normalizeLimit(kwargs.limit, 20)),
|
|
19
|
+
});
|