@agentrhq/webcmd 0.2.1 → 0.2.2
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/README.md +39 -24
- package/cli-manifest.json +504 -0
- package/clis/_shared/site-auth.js +6 -1
- package/clis/district/_lib.js +566 -0
- package/clis/district/auth.js +49 -0
- package/clis/district/checkout.js +278 -0
- package/clis/district/listings.js +158 -0
- package/clis/district/locations.js +211 -0
- package/clis/district/search.js +218 -0
- package/clis/district/seats.js +233 -0
- package/clis/district/set-location.js +82 -0
- package/clis/district/showtimes.js +433 -0
- package/clis/reddit/popular.js +12 -1
- package/clis/reddit/popular.test.js +12 -3
- package/dist/src/browser/bridge.d.ts +1 -0
- package/dist/src/browser/bridge.js +1 -1
- package/dist/src/browser/page.d.ts +4 -1
- package/dist/src/browser/page.js +12 -1
- package/dist/src/browser/protocol.d.ts +2 -0
- package/dist/src/browser/runtime/local-cloak/actions.js +1 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +2 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.js +12 -2
- package/dist/src/browser/runtime/local-cloak/session-manager.test.js +58 -0
- package/dist/src/build-manifest.js +1 -0
- package/dist/src/build-manifest.test.js +34 -0
- package/dist/src/cli.js +111 -28
- package/dist/src/discovery.js +1 -0
- package/dist/src/engine.test.js +62 -0
- package/dist/src/execution.js +1 -1
- package/dist/src/manifest-types.d.ts +2 -0
- package/dist/src/registry.d.ts +10 -0
- package/dist/src/registry.js +5 -3
- package/dist/src/registry.test.js +25 -0
- package/dist/src/runtime.d.ts +2 -0
- package/dist/src/runtime.js +1 -0
- package/dist/src/skills.d.ts +23 -5
- package/dist/src/skills.js +87 -45
- package/dist/src/skills.test.js +80 -23
- package/package.json +2 -2
- package/skills/smart-search/SKILL.md +156 -0
- package/skills/smart-search/references/sources-ai.md +74 -0
- package/skills/smart-search/references/sources-info.md +43 -0
- package/skills/smart-search/references/sources-media.md +40 -0
- package/skills/smart-search/references/sources-other.md +32 -0
- package/skills/smart-search/references/sources-shopping.md +21 -0
- package/skills/smart-search/references/sources-social.md +36 -0
- package/skills/smart-search/references/sources-tech.md +38 -0
- package/skills/smart-search/references/sources-travel.md +26 -0
- package/skills/webcmd-adapter-author/SKILL.md +1 -0
- package/skills/webcmd-adapter-author/references/adapter-template.md +2 -0
- package/skills/webcmd-autofix/SKILL.md +8 -0
- package/skills/webcmd-sitemap-author/SKILL.md +1 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { cli, Strategy } from '@agentrhq/webcmd/registry';
|
|
2
|
+
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@agentrhq/webcmd/errors';
|
|
3
|
+
|
|
4
|
+
const BASE = 'https://www.district.in';
|
|
5
|
+
const GUEST_TOKEN = '1212';
|
|
6
|
+
const DEFAULT_LOCATION = {
|
|
7
|
+
cityId: 1,
|
|
8
|
+
pCityId: '57',
|
|
9
|
+
long: 77.1165992,
|
|
10
|
+
lat: 28.4080424,
|
|
11
|
+
pCityKey: 'gurgaon',
|
|
12
|
+
pStateKey: 'haryana',
|
|
13
|
+
countryId: '1',
|
|
14
|
+
placeType: 'GOOGLE_PLACE',
|
|
15
|
+
placeId: 'ChIJQ3GqXwAZDTkR2IGBwuLDcCk',
|
|
16
|
+
subzoneId: '645',
|
|
17
|
+
cityName: 'Delhi NCR',
|
|
18
|
+
pCityName: 'Gurugram',
|
|
19
|
+
};
|
|
20
|
+
const TAB_MAP = new Map([
|
|
21
|
+
['all', 'all'],
|
|
22
|
+
['dining', 'dining'],
|
|
23
|
+
['events', 'events'],
|
|
24
|
+
['movies', 'movies'],
|
|
25
|
+
['stores', 'shopping'],
|
|
26
|
+
['shopping', 'shopping'],
|
|
27
|
+
['activities', 'attraction'],
|
|
28
|
+
['attraction', 'attraction'],
|
|
29
|
+
['play', 'play'],
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
function validateQuery(raw) {
|
|
33
|
+
const query = String(raw || '').trim();
|
|
34
|
+
if (!query) throw new ArgumentError('query is required');
|
|
35
|
+
if (query.length > 120) throw new ArgumentError('query must be 120 characters or fewer');
|
|
36
|
+
return query;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function validateLimit(raw) {
|
|
40
|
+
const limit = Number(raw ?? 20);
|
|
41
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
42
|
+
throw new ArgumentError('limit must be an integer from 1 to 100');
|
|
43
|
+
}
|
|
44
|
+
return limit;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function validateTab(raw) {
|
|
48
|
+
const value = String(raw || 'all').trim().toLowerCase();
|
|
49
|
+
const tab = TAB_MAP.get(value);
|
|
50
|
+
if (!tab) {
|
|
51
|
+
throw new ArgumentError('tab must be one of all, dining, events, movies, stores, activities, or play');
|
|
52
|
+
}
|
|
53
|
+
return tab;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function slugify(value) {
|
|
57
|
+
return String(value || '')
|
|
58
|
+
.trim()
|
|
59
|
+
.replace(/[\W_]+/g, '-')
|
|
60
|
+
.replace(/^-+|-+$/g, '')
|
|
61
|
+
.toLowerCase();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function absoluteUrl(path) {
|
|
65
|
+
if (!path) return '';
|
|
66
|
+
try {
|
|
67
|
+
return new URL(path, BASE).toString();
|
|
68
|
+
} catch {
|
|
69
|
+
return '';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function categoryFor(item) {
|
|
74
|
+
const display = String(item.domain_title || item.display_subtitle || '').trim();
|
|
75
|
+
if (display) return display.toLowerCase();
|
|
76
|
+
return String(item.entity_type || '')
|
|
77
|
+
.replace(/^EntityType/i, '')
|
|
78
|
+
.replace(/^Res$/i, 'restaurant')
|
|
79
|
+
.toLowerCase() || 'result';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function urlFor(item) {
|
|
83
|
+
const meta = item.metadata || {};
|
|
84
|
+
const type = String(item.entity_type || '');
|
|
85
|
+
if (meta.web_deeplink) return absoluteUrl(meta.web_deeplink);
|
|
86
|
+
if (type === 'EntityTypeMovie') return absoluteUrl(`/movies/${slugify(item.display_title)}-movie-tickets-MV${item.id}`);
|
|
87
|
+
if (type === 'EntityTypeEvent') return absoluteUrl(`/events/${meta.slug || slugify(item.display_title)}-buy-tickets`);
|
|
88
|
+
if (type === 'EntityTypeArtist') return absoluteUrl(`/events/${meta.slug || slugify(item.display_title)}/artist`);
|
|
89
|
+
if (type === 'EntityTypeRes') return absoluteUrl(`/dining${meta.seo_url || ''}`);
|
|
90
|
+
if (type === 'EntityTypeStore') return absoluteUrl(`/stores/${meta.slug || ''}`);
|
|
91
|
+
return '';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function locationHeaders() {
|
|
95
|
+
const loc = DEFAULT_LOCATION;
|
|
96
|
+
return {
|
|
97
|
+
'x-city-id': String(loc.cityId),
|
|
98
|
+
'x-pcity-id': String(loc.pCityId),
|
|
99
|
+
'x-user-lng': String(loc.long),
|
|
100
|
+
'x-user-lat': String(loc.lat),
|
|
101
|
+
'x-pcity-key': loc.pCityKey,
|
|
102
|
+
'x-pstate-key': loc.pStateKey,
|
|
103
|
+
'x-country-id': loc.countryId,
|
|
104
|
+
'x-place-type': loc.placeType,
|
|
105
|
+
'x-place-id': loc.placeId,
|
|
106
|
+
'x-gps-lat': String(loc.lat),
|
|
107
|
+
'x-gps-lng': String(loc.long),
|
|
108
|
+
'x-subzone-id': loc.subzoneId,
|
|
109
|
+
'x-available-tabs': 'movies,events,dining,attr_home,attraction,play,shopping,ipl',
|
|
110
|
+
'x-city-name': loc.cityName,
|
|
111
|
+
'x-pcity-name': loc.pCityName,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function headersFor(tab) {
|
|
116
|
+
return {
|
|
117
|
+
Accept: 'application/json, text/plain, */*',
|
|
118
|
+
'Accept-Language': 'en-IN,en;q=0.9',
|
|
119
|
+
'Content-Type': 'application/json',
|
|
120
|
+
Origin: BASE,
|
|
121
|
+
Referer: `${BASE}/search`,
|
|
122
|
+
'User-Agent': 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome Safari/537.36',
|
|
123
|
+
'x-guest-token': GUEST_TOKEN,
|
|
124
|
+
'x-app-type': 'ed_web',
|
|
125
|
+
'x-client-id': 'district-web',
|
|
126
|
+
'x-app-version': '11.11.1',
|
|
127
|
+
'x-device-id': crypto.randomUUID(),
|
|
128
|
+
'x-is-granular-loc': 'false',
|
|
129
|
+
'x-is-events-supported': 'true',
|
|
130
|
+
'x-is-movies-supported': 'true',
|
|
131
|
+
'x-is-dining-supported': 'true',
|
|
132
|
+
'x-gps-permission-given': '0',
|
|
133
|
+
'x-available-tabs': tab === 'all' ? 'movies,events,dining' : tab,
|
|
134
|
+
...locationHeaders(),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function rowFor(item, index) {
|
|
139
|
+
const meta = item.metadata || {};
|
|
140
|
+
const url = urlFor(item);
|
|
141
|
+
return {
|
|
142
|
+
rank: index + 1,
|
|
143
|
+
title: String(item.display_title || '').trim(),
|
|
144
|
+
category: categoryFor(item),
|
|
145
|
+
date: String(meta.date_string_v2 || '').trim(),
|
|
146
|
+
venue: String(meta.venue_name || meta.address || meta.poi_display_title || '').trim(),
|
|
147
|
+
price: String(meta.price_string_v2 || meta.offer || meta.instore_offer_text || '').trim(),
|
|
148
|
+
url,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
cli({
|
|
153
|
+
site: 'district',
|
|
154
|
+
name: 'search',
|
|
155
|
+
aliases: ['s'],
|
|
156
|
+
access: 'read',
|
|
157
|
+
description: 'Search District by Zomato across movies, events, dining, stores, activities, and play',
|
|
158
|
+
domain: 'www.district.in',
|
|
159
|
+
strategy: Strategy.PUBLIC,
|
|
160
|
+
browser: false,
|
|
161
|
+
args: [
|
|
162
|
+
{
|
|
163
|
+
name: 'query',
|
|
164
|
+
positional: true,
|
|
165
|
+
required: true,
|
|
166
|
+
help: 'Search query, for example "hamlet" or "arijit"',
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: 'limit',
|
|
170
|
+
type: 'int',
|
|
171
|
+
default: 20,
|
|
172
|
+
help: 'Maximum rows to return (1-100)',
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'tab',
|
|
176
|
+
default: 'all',
|
|
177
|
+
help: 'Search tab: all, dining, events, movies, stores, activities, or play',
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
columns: ['rank', 'title', 'category', 'date', 'venue', 'price', 'url'],
|
|
181
|
+
func: async (args) => {
|
|
182
|
+
const query = validateQuery(args.query);
|
|
183
|
+
const limit = validateLimit(args.limit);
|
|
184
|
+
const tab = validateTab(args.tab);
|
|
185
|
+
const body = {
|
|
186
|
+
get_search_results_request_type: 1,
|
|
187
|
+
post_body: { hp_selected_tab_id: tab === 'attraction' ? 'attractions_home' : 'home_v2' },
|
|
188
|
+
search_id: crypto.randomUUID(),
|
|
189
|
+
keyword: query,
|
|
190
|
+
tab_id: tab,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const resp = await fetch(`${BASE}/gw/web/search`, {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers: headersFor(tab),
|
|
196
|
+
body: JSON.stringify(body),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
if (!resp.ok) {
|
|
200
|
+
throw new CommandExecutionError(`district search request failed: HTTP ${resp.status}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const data = await resp.json();
|
|
204
|
+
if (data?.status?.status === 'STATUS_FAILURE') {
|
|
205
|
+
throw new CommandExecutionError(data.status.message || 'district search request failed');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const items = Array.isArray(data?.results) ? data.results : [];
|
|
209
|
+
if (!items.length) {
|
|
210
|
+
throw new EmptyResultError('district search', `No results found for "${query}"`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return items
|
|
214
|
+
.map(rowFor)
|
|
215
|
+
.filter((row) => row.title && row.url)
|
|
216
|
+
.slice(0, limit);
|
|
217
|
+
},
|
|
218
|
+
});
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { cli, Strategy } from '@agentrhq/webcmd/registry';
|
|
2
|
+
import { ArgumentError, EmptyResultError } from '@agentrhq/webcmd/errors';
|
|
3
|
+
import {
|
|
4
|
+
makeSeatUrl,
|
|
5
|
+
openSeatMap,
|
|
6
|
+
resolveSeatTarget,
|
|
7
|
+
validateTimeout,
|
|
8
|
+
} from './_lib.js';
|
|
9
|
+
|
|
10
|
+
const DEFAULT_TIMEOUT_SECONDS = 30;
|
|
11
|
+
const DEFAULT_LIMIT = 100;
|
|
12
|
+
|
|
13
|
+
function validateLimit(raw) {
|
|
14
|
+
const limit = Number(raw ?? DEFAULT_LIMIT);
|
|
15
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 300) {
|
|
16
|
+
throw new ArgumentError('limit must be an integer from 1 to 300');
|
|
17
|
+
}
|
|
18
|
+
return limit;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function validateCount(raw) {
|
|
22
|
+
if (raw == null || raw === '') return 0;
|
|
23
|
+
const count = Number(raw);
|
|
24
|
+
if (!Number.isInteger(count) || count < 1 || count > 10) {
|
|
25
|
+
throw new ArgumentError('count must be an integer from 1 to 10');
|
|
26
|
+
}
|
|
27
|
+
return count;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function validateMoney(raw, name) {
|
|
31
|
+
if (raw == null || raw === '') return 0;
|
|
32
|
+
const value = Number(raw);
|
|
33
|
+
if (!Number.isFinite(value) || value < 0) throw new ArgumentError(`${name} must be a positive number`);
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeClass(raw) {
|
|
38
|
+
return String(raw || '')
|
|
39
|
+
.trim()
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.replace(/[^a-z0-9]+/g, ' ')
|
|
42
|
+
.replace(/\s+/g, ' ')
|
|
43
|
+
.trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function validateBoolean(raw, name) {
|
|
47
|
+
if (raw == null || raw === '' || raw === false) return false;
|
|
48
|
+
if (raw === true) return true;
|
|
49
|
+
const value = String(raw).trim().toLowerCase();
|
|
50
|
+
if (['1', 'true', 'yes', 'y', 'on'].includes(value)) return true;
|
|
51
|
+
if (['0', 'false', 'no', 'n', 'off'].includes(value)) return false;
|
|
52
|
+
throw new ArgumentError(`${name} must be a boolean flag`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function extractSeats(page, target, url) {
|
|
56
|
+
return page.evaluate(`
|
|
57
|
+
(() => {
|
|
58
|
+
const showId = ${JSON.stringify(target.showId)};
|
|
59
|
+
const formatId = ${JSON.stringify(target.formatId)};
|
|
60
|
+
const url = ${JSON.stringify(url)};
|
|
61
|
+
const parseAria = (aria) => {
|
|
62
|
+
const seatClass = (aria.match(/class\\s+(.+?),\\s+row/i) || [])[1] || '';
|
|
63
|
+
const row = (aria.match(/row\\s+([^,]+),\\s+column/i) || [])[1] || '';
|
|
64
|
+
const column = Number((aria.match(/column\\s+(\\d+)/i) || [])[1] || 0);
|
|
65
|
+
const price = Number((aria.match(/price\\s+(\\d+)/i) || [])[1] || 0);
|
|
66
|
+
const flags = [];
|
|
67
|
+
if (/disabled friendly/i.test(aria)) flags.push('disabled_friendly');
|
|
68
|
+
if (/wheel\\s*companion|wheelCompanion/i.test(aria)) flags.push('wheel_companion');
|
|
69
|
+
return { seatClass: seatClass.trim(), row: row.trim(), column, price, flags };
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return [...document.querySelectorAll('#available-seat')].map((el, index) => {
|
|
73
|
+
const aria = el.getAttribute('aria-label') || '';
|
|
74
|
+
const label = (el.querySelector('label')?.innerText || el.innerText || '').replace(/\\s+/g, ' ').trim();
|
|
75
|
+
const parsed = parseAria(aria);
|
|
76
|
+
return {
|
|
77
|
+
rank: index + 1,
|
|
78
|
+
seat: parsed.row && label ? parsed.row + label : label,
|
|
79
|
+
row: parsed.row,
|
|
80
|
+
number: label,
|
|
81
|
+
column: parsed.column,
|
|
82
|
+
seatClass: parsed.seatClass,
|
|
83
|
+
price: parsed.price,
|
|
84
|
+
status: 'available',
|
|
85
|
+
flags: parsed.flags.join(','),
|
|
86
|
+
showId,
|
|
87
|
+
formatId,
|
|
88
|
+
url,
|
|
89
|
+
};
|
|
90
|
+
}).filter((seat) => seat.seat && seat.row && seat.number && seat.seatClass);
|
|
91
|
+
})()
|
|
92
|
+
`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function applyFilters(rows, { seatClass, maxPrice }) {
|
|
96
|
+
return rows.filter((row) => {
|
|
97
|
+
if (seatClass && !normalizeClass(row.seatClass).includes(seatClass)) return false;
|
|
98
|
+
if (maxPrice && Number(row.price) > maxPrice) return false;
|
|
99
|
+
return true;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function groupKey(row) {
|
|
104
|
+
return [row.row, row.seatClass, row.price].join('|');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function consecutiveWindow(rows, count) {
|
|
108
|
+
const sorted = [...rows].sort((a, b) => a.column - b.column);
|
|
109
|
+
for (let start = 0; start <= sorted.length - count; start += 1) {
|
|
110
|
+
const window = sorted.slice(start, start + count);
|
|
111
|
+
const consecutive = window.every((row, index) => index === 0 || row.column === window[index - 1].column + 1);
|
|
112
|
+
if (consecutive) return window;
|
|
113
|
+
}
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function chooseSeats(rows, { count, together }) {
|
|
118
|
+
if (!count) return rows;
|
|
119
|
+
if (!together) return rows.slice(0, count);
|
|
120
|
+
|
|
121
|
+
const grouped = new Map();
|
|
122
|
+
for (const row of rows) {
|
|
123
|
+
const key = groupKey(row);
|
|
124
|
+
grouped.set(key, [...(grouped.get(key) || []), row]);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const group of grouped.values()) {
|
|
128
|
+
const picked = consecutiveWindow(group, count);
|
|
129
|
+
if (picked.length) return picked;
|
|
130
|
+
}
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
cli({
|
|
135
|
+
site: 'district',
|
|
136
|
+
name: 'seats',
|
|
137
|
+
access: 'read',
|
|
138
|
+
description: 'List available seats for a District movie showtime',
|
|
139
|
+
domain: 'www.district.in',
|
|
140
|
+
strategy: Strategy.COOKIE,
|
|
141
|
+
browser: true,
|
|
142
|
+
navigateBefore: false,
|
|
143
|
+
defaultWindowMode: 'foreground',
|
|
144
|
+
siteSession: 'persistent',
|
|
145
|
+
args: [
|
|
146
|
+
{
|
|
147
|
+
name: 'show',
|
|
148
|
+
positional: true,
|
|
149
|
+
required: true,
|
|
150
|
+
help: 'District seat-layout URL or showId from district showtimes',
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'format-id',
|
|
154
|
+
help: 'District formatId from showtimes; required when show is a showId',
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: 'content-id',
|
|
158
|
+
help: 'District content id; required when show is a showId',
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: 'class',
|
|
162
|
+
help: 'Optional seat class filter, e.g. premium, premium xl, or recliner',
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'count',
|
|
166
|
+
type: 'int',
|
|
167
|
+
help: 'Number of seats to choose (1-10); without count, seats are listed normally',
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: 'together',
|
|
171
|
+
help: 'Require selected seats to be adjacent when count is provided',
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: 'max-price',
|
|
175
|
+
type: 'float',
|
|
176
|
+
help: 'Maximum price per seat',
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: 'limit',
|
|
180
|
+
type: 'int',
|
|
181
|
+
default: DEFAULT_LIMIT,
|
|
182
|
+
help: 'Maximum seats to return (1-300)',
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: 'timeout',
|
|
186
|
+
type: 'int',
|
|
187
|
+
default: DEFAULT_TIMEOUT_SECONDS,
|
|
188
|
+
help: 'Maximum seconds to wait for the seat map to render',
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
columns: [
|
|
192
|
+
'rank',
|
|
193
|
+
'seat',
|
|
194
|
+
'row',
|
|
195
|
+
'number',
|
|
196
|
+
'column',
|
|
197
|
+
'seatClass',
|
|
198
|
+
'price',
|
|
199
|
+
'status',
|
|
200
|
+
'flags',
|
|
201
|
+
'showId',
|
|
202
|
+
'formatId',
|
|
203
|
+
'url',
|
|
204
|
+
],
|
|
205
|
+
func: async (page, args) => {
|
|
206
|
+
const target = resolveSeatTarget(args);
|
|
207
|
+
const limit = validateLimit(args.limit);
|
|
208
|
+
const count = validateCount(args.count);
|
|
209
|
+
const timeout = validateTimeout(args.timeout, { def: DEFAULT_TIMEOUT_SECONDS, min: 5, max: 180 });
|
|
210
|
+
const seatClass = normalizeClass(args.class);
|
|
211
|
+
const together = validateBoolean(args.together, 'together');
|
|
212
|
+
const maxPrice = validateMoney(args['max-price'], 'max-price');
|
|
213
|
+
const url = makeSeatUrl(target);
|
|
214
|
+
|
|
215
|
+
await openSeatMap(page, target, timeout);
|
|
216
|
+
|
|
217
|
+
let rows = await extractSeats(page, target, url);
|
|
218
|
+
rows = applyFilters(rows, { seatClass, maxPrice });
|
|
219
|
+
rows = chooseSeats(rows, { count, together }).slice(0, limit).map((row, index) => ({ ...row, rank: index + 1 }));
|
|
220
|
+
|
|
221
|
+
if (!rows.length) {
|
|
222
|
+
const constraints = [
|
|
223
|
+
seatClass ? `class matching "${seatClass}"` : '',
|
|
224
|
+
maxPrice ? `price <= ${maxPrice}` : '',
|
|
225
|
+
count ? `${count} seat${count === 1 ? '' : 's'}` : '',
|
|
226
|
+
together ? 'together' : '',
|
|
227
|
+
].filter(Boolean).join(', ');
|
|
228
|
+
const detail = constraints ? `No available seats found for ${constraints}` : 'No available seats found';
|
|
229
|
+
throw new EmptyResultError('district seats', detail);
|
|
230
|
+
}
|
|
231
|
+
return rows;
|
|
232
|
+
},
|
|
233
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { cli, Strategy } from '@agentrhq/webcmd/registry';
|
|
2
|
+
import { ArgumentError, CommandExecutionError } from '@agentrhq/webcmd/errors';
|
|
3
|
+
import {
|
|
4
|
+
applyLocationViaPicker,
|
|
5
|
+
validateTimeout,
|
|
6
|
+
} from './_lib.js';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_TIMEOUT_SECONDS = 45;
|
|
9
|
+
|
|
10
|
+
function validateLocation(raw) {
|
|
11
|
+
const location = String(raw || '').trim();
|
|
12
|
+
if (!location) throw new ArgumentError('location is required');
|
|
13
|
+
if (location.length > 120) throw new ArgumentError('location must be 120 characters or fewer');
|
|
14
|
+
return location;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function validateRank(raw) {
|
|
18
|
+
const rank = Number(raw ?? 1);
|
|
19
|
+
if (!Number.isInteger(rank) || rank < 1 || rank > 20) {
|
|
20
|
+
throw new ArgumentError('rank must be an integer from 1 to 20');
|
|
21
|
+
}
|
|
22
|
+
return rank;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
cli({
|
|
26
|
+
site: 'district',
|
|
27
|
+
name: 'set-location',
|
|
28
|
+
aliases: ['setlocation'],
|
|
29
|
+
access: 'write',
|
|
30
|
+
description: 'Set the District browser session location for movie booking filters',
|
|
31
|
+
domain: 'www.district.in',
|
|
32
|
+
strategy: Strategy.COOKIE,
|
|
33
|
+
browser: true,
|
|
34
|
+
navigateBefore: false,
|
|
35
|
+
defaultWindowMode: 'foreground',
|
|
36
|
+
siteSession: 'persistent',
|
|
37
|
+
args: [
|
|
38
|
+
{
|
|
39
|
+
name: 'location',
|
|
40
|
+
positional: true,
|
|
41
|
+
required: true,
|
|
42
|
+
help: 'City, area, mall, or locality, for example "Bangalore" or "Indiranagar"',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'rank',
|
|
46
|
+
type: 'int',
|
|
47
|
+
default: 1,
|
|
48
|
+
help: 'Pick the Nth District location result (1-20), default: 1',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'timeout',
|
|
52
|
+
type: 'int',
|
|
53
|
+
default: DEFAULT_TIMEOUT_SECONDS,
|
|
54
|
+
help: 'Maximum seconds to wait for the picker and location change',
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
columns: [
|
|
58
|
+
'status',
|
|
59
|
+
'name',
|
|
60
|
+
'city',
|
|
61
|
+
'state',
|
|
62
|
+
'cityKey',
|
|
63
|
+
'cityId',
|
|
64
|
+
'placeId',
|
|
65
|
+
'subzoneId',
|
|
66
|
+
'lat',
|
|
67
|
+
'lng',
|
|
68
|
+
'availableTabs',
|
|
69
|
+
'source',
|
|
70
|
+
],
|
|
71
|
+
func: async (page, args) => {
|
|
72
|
+
const location = validateLocation(args.location);
|
|
73
|
+
const rank = validateRank(args.rank);
|
|
74
|
+
const timeout = validateTimeout(args.timeout, { def: DEFAULT_TIMEOUT_SECONDS, min: 10, max: 180 });
|
|
75
|
+
|
|
76
|
+
const row = await applyLocationViaPicker(page, location, timeout, rank);
|
|
77
|
+
if (!row.cityKey || !row.cityId) {
|
|
78
|
+
throw new CommandExecutionError('District location was selected, but normalized city metadata could not be read');
|
|
79
|
+
}
|
|
80
|
+
return row;
|
|
81
|
+
},
|
|
82
|
+
});
|