@agentrhq/webcmd 0.4.1 → 0.4.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 (74) hide show
  1. package/README.md +3 -0
  2. package/cli-manifest.json +955 -96
  3. package/clis/_shared/site-auth.js +0 -1
  4. package/clis/_shared/site-auth.test.js +0 -1
  5. package/clis/amazon-in/auth.js +44 -0
  6. package/clis/amazon-in/checkout-status.js +54 -0
  7. package/clis/amazon-in/checkout.js +478 -0
  8. package/clis/amazon-in/parsers.js +252 -0
  9. package/clis/amazon-in/parsers.test.js +230 -0
  10. package/clis/amazon-in/product.js +72 -0
  11. package/clis/amazon-in/search.js +76 -0
  12. package/clis/amazon-in/shared.js +40 -0
  13. package/clis/amazon-in/wishlist.js +98 -0
  14. package/clis/blinkit/checkout.js +0 -1
  15. package/clis/blinkit/place-order.js +0 -1
  16. package/clis/chatgpt/model.js +2 -2
  17. package/clis/chatgpt/model.test.js +7 -1
  18. package/clis/chatgpt/utils.js +8 -0
  19. package/clis/chatgpt/utils.test.js +92 -1
  20. package/clis/district/checkout.js +0 -1
  21. package/clis/district/seats.js +0 -1
  22. package/clis/district/set-location.js +0 -1
  23. package/clis/district/showtimes.js +0 -1
  24. package/clis/google/images.js +456 -0
  25. package/clis/google/images.test.js +375 -0
  26. package/clis/instagram/user.js +5 -13
  27. package/clis/instagram/user.test.js +66 -0
  28. package/clis/mercury/check-login.js +0 -1
  29. package/clis/mercury/reimbursement-draft.js +0 -1
  30. package/clis/practo/book-confirm.js +0 -1
  31. package/clis/practo/cancel.js +0 -1
  32. package/clis/trip/attraction.js +74 -0
  33. package/clis/trip/car.js +74 -0
  34. package/clis/trip/deals.js +61 -0
  35. package/clis/trip/flight-round.js +88 -0
  36. package/clis/trip/flight.js +83 -0
  37. package/clis/trip/hotel-search.js +80 -0
  38. package/clis/trip/hotel.js +54 -0
  39. package/clis/trip/package.js +93 -0
  40. package/clis/trip/search.js +43 -0
  41. package/clis/trip/tour.js +84 -0
  42. package/clis/trip/train.js +76 -0
  43. package/clis/trip/transfer.js +82 -0
  44. package/clis/trip/trip.test.js +1420 -0
  45. package/clis/trip/utils.js +911 -0
  46. package/dist/src/build-manifest.js +0 -1
  47. package/dist/src/build-manifest.test.js +4 -0
  48. package/dist/src/cli.js +7 -7
  49. package/dist/src/cli.test.js +26 -10
  50. package/dist/src/command-presentation.js +1 -1
  51. package/dist/src/command-presentation.test.js +3 -0
  52. package/dist/src/command-surface.js +1 -1
  53. package/dist/src/command-surface.test.js +4 -0
  54. package/dist/src/commands/auth.js +0 -2
  55. package/dist/src/commands/auth.test.js +0 -2
  56. package/dist/src/discovery.js +0 -1
  57. package/dist/src/execution.js +3 -3
  58. package/dist/src/execution.test.js +68 -15
  59. package/dist/src/hosted/browser-args.js +2 -2
  60. package/dist/src/hosted/browser-args.test.js +10 -0
  61. package/dist/src/hosted/client.d.ts +8 -10
  62. package/dist/src/hosted/client.js +26 -34
  63. package/dist/src/hosted/client.test.js +79 -24
  64. package/dist/src/hosted/runner.js +7 -35
  65. package/dist/src/hosted/runner.test.js +27 -51
  66. package/dist/src/hosted/types.d.ts +1 -5
  67. package/dist/src/manifest-types.d.ts +0 -2
  68. package/dist/src/registry.d.ts +0 -2
  69. package/dist/src/registry.js +0 -1
  70. package/dist/src/root-command-surface.js +10 -2
  71. package/hosted-contract.json +2627 -1535
  72. package/package.json +2 -2
  73. package/skills/webcmd-browser/SKILL.md +2 -1
  74. package/skills/webcmd-usage/SKILL.md +1 -1
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Trip.com (international) running-promotions listing.
3
+ *
4
+ * Trip.com curates its live campaigns on a "Today's Top Deals" hub
5
+ * (`/sale/deals/`), each a promo tile linking to a dedicated campaign page. The
6
+ * hub renders client-side, so this reads the rendered `.top-deals_link-item`
7
+ * tiles (title, offer line, parsed discount, campaign URL) the same way the other
8
+ * browser-mode trip commands read their result cards. The per-campaign terms and
9
+ * bookable inventory live on the linked page and are out of scope here.
10
+ */
11
+ import { AuthRequiredError, CommandExecutionError } from '@agentrhq/webcmd/errors';
12
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
13
+ import { WAIT_FOR_DEALS_JS, buildDealsExtractJs, buildDealsUrl, parseListLimit } from './utils.js';
14
+
15
+ cli({
16
+ site: 'trip',
17
+ name: 'deals',
18
+ access: 'read',
19
+ description: 'List Trip.com live promotions from the Top Deals hub: campaign title, offer, discount, and link',
20
+ domain: 'trip.com',
21
+ strategy: Strategy.COOKIE,
22
+ browser: true,
23
+ navigateBefore: false,
24
+ args: [
25
+ { name: 'limit', type: 'int', default: 20, help: 'Number of deals (1-50)' },
26
+ ],
27
+ columns: [
28
+ 'rank',
29
+ 'title', 'offer',
30
+ 'discount',
31
+ 'url',
32
+ ],
33
+ func: async (page, kwargs) => {
34
+ const limit = parseListLimit(kwargs.limit);
35
+
36
+ await page.goto(buildDealsUrl());
37
+ const waitResult = await page.evaluate(WAIT_FOR_DEALS_JS);
38
+ if (waitResult === 'captcha') {
39
+ throw new AuthRequiredError('trip.com', 'Trip.com is asking for a verification; complete it in your browser session and retry');
40
+ }
41
+ if (waitResult !== 'content') {
42
+ throw new CommandExecutionError(`Trip.com deals page did not render deal tiles (state=${String(waitResult)})`);
43
+ }
44
+ const raw = await page.evaluate(buildDealsExtractJs());
45
+ if (!Array.isArray(raw)) {
46
+ throw new CommandExecutionError('Trip.com deals DOM extraction returned malformed rows');
47
+ }
48
+ // The Top Deals hub is a permanent curated page, so once the wait confirms it
49
+ // rendered, zero parsed tiles means the tile markup drifted, not an empty hub.
50
+ if (raw.length === 0) {
51
+ throw new CommandExecutionError('Trip.com deals hub rendered but no promotion tiles parsed (the tile markup may have changed)');
52
+ }
53
+ return raw.slice(0, limit).map((r, i) => ({
54
+ rank: i + 1,
55
+ title: r.title,
56
+ offer: r.offer,
57
+ discount: r.discount,
58
+ url: r.url,
59
+ }));
60
+ },
61
+ });
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Trip.com (international) round-trip flight search by IATA route + dates.
3
+ *
4
+ * Complements one-way `flight`: the round-trip results page renders the same
5
+ * `.result-item` outbound cards (priced for the round trip), so this reuses the
6
+ * shared flight extractor and wait helper against a `triptype=rt` search URL.
7
+ * Rows missing the airline, both airports, or both times are dropped.
8
+ */
9
+ import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@agentrhq/webcmd/errors';
10
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
11
+ import {
12
+ WAIT_FOR_FLIGHTS_JS,
13
+ buildFlightExtractJs,
14
+ buildFlightRoundSearchUrl,
15
+ parseIataCode,
16
+ parseIsoDate,
17
+ parseListLimit,
18
+ } from './utils.js';
19
+
20
+ cli({
21
+ site: 'trip',
22
+ name: 'flight-round',
23
+ access: 'read',
24
+ description: 'Search Trip.com round-trip flights by IATA route + depart/return dates',
25
+ domain: 'trip.com',
26
+ strategy: Strategy.COOKIE,
27
+ browser: true,
28
+ navigateBefore: false,
29
+ args: [
30
+ { name: 'from', required: true, positional: true, help: 'Departure IATA code (e.g. LON / LHR)' },
31
+ { name: 'to', required: true, positional: true, help: 'Arrival IATA code (e.g. NYC / JFK)' },
32
+ { name: 'depart', required: true, help: 'Outbound date (YYYY-MM-DD)' },
33
+ { name: 'return', required: true, help: 'Return date (YYYY-MM-DD)' },
34
+ { name: 'limit', type: 'int', default: 20, help: 'Number of flights (1-50)' },
35
+ ],
36
+ columns: [
37
+ 'rank',
38
+ 'airline',
39
+ 'departureTime', 'departureAirport',
40
+ 'arrivalTime', 'arrivalAirport',
41
+ 'duration', 'stops',
42
+ 'price', 'currency',
43
+ 'url',
44
+ ],
45
+ func: async (page, kwargs) => {
46
+ const fromCode = parseIataCode('from', kwargs.from);
47
+ const toCode = parseIataCode('to', kwargs.to);
48
+ if (fromCode === toCode) {
49
+ throw new ArgumentError(`--from and --to must differ (got ${fromCode})`);
50
+ }
51
+ const depart = parseIsoDate('depart', kwargs.depart);
52
+ const ret = parseIsoDate('return', kwargs.return);
53
+ if (depart >= ret) {
54
+ throw new ArgumentError(`--depart must be before --return (got ${depart} .. ${ret})`);
55
+ }
56
+ const limit = parseListLimit(kwargs.limit);
57
+
58
+ const searchUrl = buildFlightRoundSearchUrl(fromCode, toCode, depart, ret);
59
+ await page.goto(searchUrl);
60
+ const waitResult = await page.evaluate(WAIT_FOR_FLIGHTS_JS);
61
+ if (waitResult === 'captcha') {
62
+ throw new AuthRequiredError('trip.com', 'Trip.com is asking for a verification; complete it in your browser session and retry');
63
+ }
64
+ if (waitResult !== 'content') {
65
+ throw new CommandExecutionError(`Trip.com flight page did not render flight cards (state=${String(waitResult)})`);
66
+ }
67
+ const raw = await page.evaluate(buildFlightExtractJs());
68
+ if (!Array.isArray(raw)) {
69
+ throw new CommandExecutionError('Trip.com flight DOM extraction returned malformed rows');
70
+ }
71
+ if (raw.length === 0) {
72
+ throw new EmptyResultError('trip flight-round', `No round-trip flights for ${fromCode} to ${toCode} on ${depart} .. ${ret}`);
73
+ }
74
+ return raw.slice(0, limit).map((r, i) => ({
75
+ rank: i + 1,
76
+ airline: r.airline,
77
+ departureTime: r.departureTime,
78
+ departureAirport: r.departureAirport,
79
+ arrivalTime: r.arrivalTime,
80
+ arrivalAirport: r.arrivalAirport,
81
+ duration: r.duration,
82
+ stops: r.stops,
83
+ price: r.price,
84
+ currency: r.currency,
85
+ url: searchUrl,
86
+ }));
87
+ },
88
+ });
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Trip.com (international) one-way flight search by IATA route + date.
3
+ *
4
+ * Trip.com is the English-facing sibling of Ctrip. Results render client-side
5
+ * into `.result-item` cards keyed by stable `data-testid` anchors, so this
6
+ * reads by selector (see `buildFlightExtractJs` in utils) rather than by
7
+ * position. Rows missing the airline, both airports, or both times are dropped.
8
+ */
9
+ import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@agentrhq/webcmd/errors';
10
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
11
+ import {
12
+ WAIT_FOR_FLIGHTS_JS,
13
+ buildFlightExtractJs,
14
+ buildFlightSearchUrl,
15
+ parseIataCode,
16
+ parseIsoDate,
17
+ parseListLimit,
18
+ } from './utils.js';
19
+
20
+ cli({
21
+ site: 'trip',
22
+ name: 'flight',
23
+ access: 'read',
24
+ description: 'Search Trip.com one-way flights by IATA route + departure date',
25
+ domain: 'trip.com',
26
+ strategy: Strategy.COOKIE,
27
+ browser: true,
28
+ navigateBefore: false,
29
+ args: [
30
+ { name: 'from', required: true, positional: true, help: 'Departure IATA code (e.g. LON / LHR)' },
31
+ { name: 'to', required: true, positional: true, help: 'Arrival IATA code (e.g. NYC / JFK)' },
32
+ { name: 'date', required: true, help: 'Departure date (YYYY-MM-DD)' },
33
+ { name: 'limit', type: 'int', default: 20, help: 'Number of flights (1-50)' },
34
+ ],
35
+ columns: [
36
+ 'rank',
37
+ 'airline',
38
+ 'departureTime', 'departureAirport',
39
+ 'arrivalTime', 'arrivalAirport',
40
+ 'duration', 'stops',
41
+ 'price', 'currency',
42
+ 'url',
43
+ ],
44
+ func: async (page, kwargs) => {
45
+ const fromCode = parseIataCode('from', kwargs.from);
46
+ const toCode = parseIataCode('to', kwargs.to);
47
+ if (fromCode === toCode) {
48
+ throw new ArgumentError(`--from and --to must differ (got ${fromCode})`);
49
+ }
50
+ const date = parseIsoDate('date', kwargs.date);
51
+ const limit = parseListLimit(kwargs.limit);
52
+
53
+ const searchUrl = buildFlightSearchUrl(fromCode, toCode, date);
54
+ await page.goto(searchUrl);
55
+ const waitResult = await page.evaluate(WAIT_FOR_FLIGHTS_JS);
56
+ if (waitResult === 'captcha') {
57
+ throw new AuthRequiredError('trip.com', 'Trip.com is asking for a verification; complete it in your browser session and retry');
58
+ }
59
+ if (waitResult !== 'content') {
60
+ throw new CommandExecutionError(`Trip.com flight page did not render flight cards (state=${String(waitResult)})`);
61
+ }
62
+ const raw = await page.evaluate(buildFlightExtractJs());
63
+ if (!Array.isArray(raw)) {
64
+ throw new CommandExecutionError('Trip.com flight DOM extraction returned malformed rows');
65
+ }
66
+ if (raw.length === 0) {
67
+ throw new EmptyResultError('trip flight', `No flights for ${fromCode} to ${toCode} on ${date}`);
68
+ }
69
+ return raw.slice(0, limit).map((r, i) => ({
70
+ rank: i + 1,
71
+ airline: r.airline,
72
+ departureTime: r.departureTime,
73
+ departureAirport: r.departureAirport,
74
+ arrivalTime: r.arrivalTime,
75
+ arrivalAirport: r.arrivalAirport,
76
+ duration: r.duration,
77
+ stops: r.stops,
78
+ price: r.price,
79
+ currency: r.currency,
80
+ url: searchUrl,
81
+ }));
82
+ },
83
+ });
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Trip.com (international) hotel listing by city id + check-in/out date range.
3
+ *
4
+ * Trip.com renders hotel results client-side into `.hotel-card` cards keyed by
5
+ * stable class fields, so this reads by selector (see `buildHotelExtractJs` in
6
+ * utils) rather than positional innerText. Cards without a hotel name are
7
+ * dropped rather than surfaced with blanks.
8
+ */
9
+ import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@agentrhq/webcmd/errors';
10
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
11
+ import {
12
+ WAIT_FOR_HOTELS_JS,
13
+ buildHotelExtractJs,
14
+ buildHotelSearchUrl,
15
+ parseCityId,
16
+ parseIsoDate,
17
+ parseListLimit,
18
+ } from './utils.js';
19
+
20
+ cli({
21
+ site: 'trip',
22
+ name: 'hotel-search',
23
+ access: 'read',
24
+ description: 'List Trip.com hotels for a city id + check-in/out date range',
25
+ domain: 'trip.com',
26
+ strategy: Strategy.COOKIE,
27
+ browser: true,
28
+ navigateBefore: false,
29
+ args: [
30
+ { name: 'city', required: true, positional: true, help: 'Numeric Trip.com city id (discover via the hotels search box; e.g. 338 for London)' },
31
+ { name: 'checkin', required: true, help: 'Check-in date (YYYY-MM-DD)' },
32
+ { name: 'checkout', required: true, help: 'Check-out date (YYYY-MM-DD)' },
33
+ { name: 'limit', type: 'int', default: 20, help: 'Number of hotels (1-50)' },
34
+ ],
35
+ columns: [
36
+ 'rank',
37
+ 'name', 'score', 'reviewLabel', 'reviews',
38
+ 'location', 'room',
39
+ 'price', 'currency',
40
+ 'url',
41
+ ],
42
+ func: async (page, kwargs) => {
43
+ const cityId = parseCityId('city', kwargs.city);
44
+ const checkin = parseIsoDate('checkin', kwargs.checkin);
45
+ const checkout = parseIsoDate('checkout', kwargs.checkout);
46
+ if (checkin >= checkout) {
47
+ throw new ArgumentError(`--checkin must be before --checkout (got ${checkin} .. ${checkout})`);
48
+ }
49
+ const limit = parseListLimit(kwargs.limit);
50
+
51
+ const searchUrl = buildHotelSearchUrl(cityId, checkin, checkout);
52
+ await page.goto(searchUrl);
53
+ const waitResult = await page.evaluate(WAIT_FOR_HOTELS_JS);
54
+ if (waitResult === 'captcha') {
55
+ throw new AuthRequiredError('trip.com', 'Trip.com is asking for a verification; complete it in your browser session and retry');
56
+ }
57
+ if (waitResult !== 'content') {
58
+ throw new CommandExecutionError(`Trip.com hotel page did not render hotel cards (state=${String(waitResult)})`);
59
+ }
60
+ const raw = await page.evaluate(buildHotelExtractJs());
61
+ if (!Array.isArray(raw)) {
62
+ throw new CommandExecutionError('Trip.com hotel DOM extraction returned malformed rows');
63
+ }
64
+ if (raw.length === 0) {
65
+ throw new EmptyResultError('trip hotel-search', `No hotels for city ${cityId} on ${checkin} .. ${checkout}`);
66
+ }
67
+ return raw.slice(0, limit).map((r, i) => ({
68
+ rank: i + 1,
69
+ name: r.name,
70
+ score: r.score,
71
+ reviewLabel: r.reviewLabel,
72
+ reviews: r.reviews,
73
+ location: r.location,
74
+ room: r.room,
75
+ price: r.price,
76
+ currency: r.currency,
77
+ url: searchUrl,
78
+ }));
79
+ },
80
+ });
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Trip.com (international) single-hotel detail by id: rating breakdown, popular
3
+ * amenities, check-in/out policy, address, and coordinates.
4
+ *
5
+ * Reads `__NEXT_DATA__.props.pageProps.hotelDetailResponse` from the SSR detail
6
+ * page, the same SSR shape the mainland `ctrip hotel` detail uses. This surfaces
7
+ * the fields the `hotel-search` list row does not carry. Room-level nightly
8
+ * prices load via a post-SSR XHR and are out of scope here; `hotel-search`
9
+ * already reports a representative nightly price per hotel.
10
+ */
11
+ import { AuthRequiredError, CommandExecutionError, EmptyResultError } from '@agentrhq/webcmd/errors';
12
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
13
+ import { WAIT_FOR_HOTEL_DETAIL_JS, buildHotelDetailExtractJs, buildHotelDetailUrl, parseHotelId } from './utils.js';
14
+
15
+ cli({
16
+ site: 'trip',
17
+ name: 'hotel',
18
+ access: 'read',
19
+ description: 'Show a Trip.com hotel detail by id (rating breakdown, amenities, check-in/out policy)',
20
+ domain: 'trip.com',
21
+ strategy: Strategy.COOKIE,
22
+ browser: true,
23
+ navigateBefore: false,
24
+ args: [
25
+ { name: 'id', required: true, positional: true, help: 'Numeric Trip.com hotel id (discover via the hotels list; e.g. 715233)' },
26
+ ],
27
+ columns: [
28
+ 'hotelId', 'name', 'enName',
29
+ 'star', 'score', 'scoreLabel', 'reviewCount', 'ratingBreakdown',
30
+ 'facilities', 'checkInOut',
31
+ 'cityName', 'address', 'lat', 'lon',
32
+ 'url',
33
+ ],
34
+ func: async (page, kwargs) => {
35
+ const hotelId = parseHotelId('id', kwargs.id);
36
+ const url = buildHotelDetailUrl(hotelId);
37
+ await page.goto(url);
38
+ const waitResult = await page.evaluate(WAIT_FOR_HOTEL_DETAIL_JS);
39
+ if (waitResult === 'captcha') {
40
+ throw new AuthRequiredError('trip.com', 'Trip.com is asking for a verification; complete it in your browser session and retry');
41
+ }
42
+ if (waitResult !== 'content') {
43
+ throw new CommandExecutionError(`Trip.com hotel detail page did not expose SSR hotel data (state=${String(waitResult)})`);
44
+ }
45
+ const detail = await page.evaluate(buildHotelDetailExtractJs());
46
+ if (!detail || typeof detail !== 'object') {
47
+ throw new CommandExecutionError('Trip.com hotel detail SSR extraction returned malformed data');
48
+ }
49
+ if (!detail.hotelId || !detail.name) {
50
+ throw new EmptyResultError('trip hotel', `No detail exposed for hotel id ${hotelId}`);
51
+ }
52
+ return [{ ...detail, url }];
53
+ },
54
+ });
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Trip.com (international) flight+hotel package search by route + dates.
3
+ *
4
+ * Trip.com prices its packages through the flight-selection step of the booking
5
+ * flow: a public, unsigned POST keyed on the metro city codes plus the
6
+ * destination hotel city id returns the outbound flight options priced at the
7
+ * bundle rate (the specific hotel is picked in a later step). So this is a plain
8
+ * public fetch (no browser) that resolves both endpoints through the same POI
9
+ * search `trip search` uses, then lists the package flights (see
10
+ * `fetchPackageSearch` in utils). Per-person package fares only; the return leg
11
+ * rides on the hotel checkout date.
12
+ */
13
+ import { ArgumentError, CommandExecutionError, EmptyResultError } from '@agentrhq/webcmd/errors';
14
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
15
+ import { fetchPackageSearch, mapPackageRow, parseIsoDate, parseKeyword, parseListLimit, resolvePackageCity } from './utils.js';
16
+
17
+ function parseAdults(raw) {
18
+ if (raw === undefined || raw === null || raw === '') return 2;
19
+ const parsed = Number(raw);
20
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 9) {
21
+ throw new ArgumentError(`--adults must be an integer between 1 and 9, got ${JSON.stringify(raw)}`);
22
+ }
23
+ return parsed;
24
+ }
25
+
26
+ cli({
27
+ site: 'trip',
28
+ name: 'package',
29
+ access: 'read',
30
+ description: 'Search Trip.com flight+hotel packages by route + dates; lists the package flight options priced at the bundle rate',
31
+ domain: 'trip.com',
32
+ strategy: Strategy.PUBLIC,
33
+ browser: false,
34
+ args: [
35
+ { name: 'from', required: true, positional: true, help: 'Origin city keyword (e.g. Seoul / London / Bangkok)' },
36
+ { name: 'to', required: true, positional: true, help: 'Destination city keyword (e.g. Tokyo / Paris / Singapore)' },
37
+ { name: 'depart', required: true, help: 'Outbound date (YYYY-MM-DD)' },
38
+ { name: 'return', required: true, help: 'Return date (YYYY-MM-DD)' },
39
+ { name: 'adults', type: 'int', default: 2, help: 'Number of adults (1-9, default 2)' },
40
+ { name: 'limit', type: 'int', default: 20, help: 'Number of packages (1-50)' },
41
+ ],
42
+ columns: [
43
+ 'rank',
44
+ 'airline', 'flightNo',
45
+ 'from', 'to',
46
+ 'departure', 'arrival',
47
+ 'stops',
48
+ 'price', 'currency',
49
+ ],
50
+ func: async (kwargs) => {
51
+ const from = parseKeyword('from', kwargs.from);
52
+ const to = parseKeyword('to', kwargs.to);
53
+ const depart = parseIsoDate('depart', kwargs.depart);
54
+ const ret = parseIsoDate('return', kwargs.return);
55
+ if (depart >= ret) {
56
+ throw new ArgumentError(`--depart must be before --return (got ${depart} .. ${ret})`);
57
+ }
58
+ const adults = parseAdults(kwargs.adults);
59
+ const limit = parseListLimit(kwargs.limit);
60
+
61
+ const origin = await resolvePackageCity(from);
62
+ if (!origin) {
63
+ throw new ArgumentError(`Could not resolve origin "${from}" to a Trip.com city; run 'trip search ${from}' to find the name`);
64
+ }
65
+ const dest = await resolvePackageCity(to);
66
+ if (!dest) {
67
+ throw new ArgumentError(`Could not resolve destination "${to}" to a Trip.com city; run 'trip search ${to}' to find the name`);
68
+ }
69
+ if (origin.cityId === dest.cityId) {
70
+ throw new ArgumentError(`--from and --to must differ (both resolved to ${dest.name})`);
71
+ }
72
+
73
+ const groups = await fetchPackageSearch({
74
+ dcode: origin.cityCode,
75
+ acode: dest.cityCode,
76
+ hcityid: String(dest.cityId),
77
+ depart,
78
+ ret,
79
+ adults,
80
+ });
81
+ if (groups.length === 0) {
82
+ throw new EmptyResultError('trip package', `No flight+hotel packages for ${origin.name} to ${dest.name} on ${depart} .. ${ret}`);
83
+ }
84
+ const rows = groups
85
+ .filter((g) => g && Array.isArray(g.flightlist) && g.flightlist.length)
86
+ .map((g) => mapPackageRow(g, 0))
87
+ .filter((row) => row.flightNo && row.from && row.to && row.departure && row.arrival);
88
+ if (rows.length === 0) {
89
+ throw new CommandExecutionError(`Trip.com package search returned ${groups.length} group(s) but none carried a parseable flight identity, route, and time`);
90
+ }
91
+ return rows.slice(0, limit).map((row, i) => ({ ...row, rank: i + 1 }));
92
+ },
93
+ });
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Trip.com (international) destination suggest.
3
+ *
4
+ * Trip.com's flight / hotel search boxes resolve a keyword through a public,
5
+ * unsigned POI-search endpoint, so this is a plain public fetch (no browser) that
6
+ * returns the city / airport matches. The `cityId` feeds `hotel-search` / `car` /
7
+ * `tour`, and the `airportCode` feeds `flight` / `transfer`, so `search` is the
8
+ * discovery step for the id-based commands (see `fetchPoiSearch` in utils).
9
+ */
10
+ import { EmptyResultError } from '@agentrhq/webcmd/errors';
11
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
12
+ import { fetchPoiSearch, flattenPoiResults, mapSearchRow, parseKeyword, parseListLimit } from './utils.js';
13
+
14
+ cli({
15
+ site: 'trip',
16
+ name: 'search',
17
+ access: 'read',
18
+ description: 'Suggest Trip.com destinations (cities, airports) for a keyword; resolves the ids the other commands take',
19
+ domain: 'trip.com',
20
+ strategy: Strategy.PUBLIC,
21
+ browser: false,
22
+ args: [
23
+ { name: 'query', required: true, positional: true, help: 'Destination keyword (e.g. Tokyo / Bali / London)' },
24
+ { name: 'limit', type: 'int', default: 20, help: 'Number of suggestions (1-50)' },
25
+ ],
26
+ columns: [
27
+ 'rank',
28
+ 'name', 'type',
29
+ 'cityId', 'airportCode',
30
+ 'province', 'country',
31
+ ],
32
+ func: async (kwargs) => {
33
+ const query = parseKeyword('query', kwargs.query);
34
+ const limit = parseListLimit(kwargs.limit);
35
+
36
+ const results = await fetchPoiSearch(query);
37
+ const items = flattenPoiResults(results).filter((item) => item && item.name);
38
+ if (items.length === 0) {
39
+ throw new EmptyResultError('trip search', `No destinations for "${query}"`);
40
+ }
41
+ return items.slice(0, limit).map((item, i) => mapSearchRow(item, i));
42
+ },
43
+ });
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Trip.com (international) tour-package search by destination keyword.
3
+ *
4
+ * Trip.com's tour results (`package-tours/list?kwd=<keyword>`) load through a
5
+ * signed POST that only fires on a search submit, so this navigates the results
6
+ * page and lets the page issue its own signed request while a fetch hook captures
7
+ * the `products` response, rather than replaying the signature (see
8
+ * `buildTourSearchJs` in utils). Per-departure pricing and availability sit behind
9
+ * the booking step; the row `price` is the starting per-person estimate shown.
10
+ */
11
+ import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@agentrhq/webcmd/errors';
12
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
13
+ import { buildTourSearchJs, buildTourSearchUrl, parseKeyword, parseListLimit } from './utils.js';
14
+
15
+ const TOUR_TABS = { private: 'privateTours', group: 'groupTours' };
16
+
17
+ function parseTourType(raw) {
18
+ if (raw === undefined || raw === null || raw === '') return TOUR_TABS.private;
19
+ const value = String(raw).trim().toLowerCase();
20
+ if (!TOUR_TABS[value]) {
21
+ throw new ArgumentError(`--type must be private or group, got ${JSON.stringify(raw)}`);
22
+ }
23
+ return TOUR_TABS[value];
24
+ }
25
+
26
+ cli({
27
+ site: 'trip',
28
+ name: 'tour',
29
+ access: 'read',
30
+ description: 'Search Trip.com tour packages by destination keyword (private or group tours)',
31
+ domain: 'trip.com',
32
+ strategy: Strategy.COOKIE,
33
+ browser: true,
34
+ navigateBefore: false,
35
+ args: [
36
+ { name: 'query', required: true, positional: true, help: 'Destination or tour keyword (e.g. Tokyo / Kyoto / Bali)' },
37
+ { name: 'type', default: 'private', help: 'Tour line: private or group (default private)' },
38
+ { name: 'limit', type: 'int', default: 20, help: 'Number of tours (1-50)' },
39
+ ],
40
+ columns: [
41
+ 'rank',
42
+ 'name', 'type',
43
+ 'rating', 'reviews',
44
+ 'price', 'currency',
45
+ 'url',
46
+ ],
47
+ func: async (page, kwargs) => {
48
+ const query = parseKeyword('query', kwargs.query);
49
+ const tourType = parseTourType(kwargs.type);
50
+ const limit = parseListLimit(kwargs.limit);
51
+
52
+ const searchUrl = buildTourSearchUrl(query, tourType);
53
+ await page.goto(searchUrl);
54
+ const result = await page.evaluate(buildTourSearchJs(query));
55
+ if (!result || typeof result !== 'object') {
56
+ throw new CommandExecutionError('Trip.com tour search returned malformed data');
57
+ }
58
+ if (result.status === 'captcha') {
59
+ throw new AuthRequiredError('trip.com', 'Trip.com is asking for a verification; complete it in your browser session and retry');
60
+ }
61
+ if (result.status === 'empty') {
62
+ throw new EmptyResultError('trip tour', `No ${kwargs.type || 'private'} tours for "${query}"`);
63
+ }
64
+ if (result.status !== 'content') {
65
+ throw new CommandExecutionError(`Trip.com tour search did not return results (state=${String(result.status)})`);
66
+ }
67
+ // Products captured but none carry a name is drift (schema moved), not an empty search;
68
+ // a genuine no-match resolves as status 'empty' above off the page's "0 routes found".
69
+ const rows = Array.isArray(result.rows) ? result.rows.filter((r) => r.name) : [];
70
+ if (rows.length === 0) {
71
+ throw new CommandExecutionError('Trip.com tour search captured products but none carried a name (the product markup may have changed)');
72
+ }
73
+ return rows.slice(0, limit).map((r, i) => ({
74
+ rank: i + 1,
75
+ name: r.name,
76
+ type: r.type,
77
+ rating: r.rating,
78
+ reviews: r.reviews,
79
+ price: r.price,
80
+ currency: 'USD',
81
+ url: r.url,
82
+ }));
83
+ },
84
+ });
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Trip.com (international) train timetable by route.
3
+ *
4
+ * Trip.com exposes per-route SEO timetables at
5
+ * `trains/<country>/route/<from>-to-<to>/`, so `--country` is required and the
6
+ * city names are slugified into the URL. The page lists journeys as timetable
7
+ * rows (departure / arrival times, stations, duration, changes) read by stable
8
+ * class fields (see `buildTrainExtractJs` in utils); per-journey fares sit behind
9
+ * the booking step and are out of scope here.
10
+ */
11
+ import { AuthRequiredError, CommandExecutionError, EmptyResultError } from '@agentrhq/webcmd/errors';
12
+ import { cli, Strategy } from '@agentrhq/webcmd/registry';
13
+ import {
14
+ WAIT_FOR_TRAINS_JS,
15
+ buildTrainExtractJs,
16
+ buildTrainRouteUrl,
17
+ parseKeyword,
18
+ parseListLimit,
19
+ } from './utils.js';
20
+
21
+ cli({
22
+ site: 'trip',
23
+ name: 'train',
24
+ access: 'read',
25
+ description: 'Show a Trip.com train route timetable (departure/arrival times, duration, changes)',
26
+ domain: 'trip.com',
27
+ strategy: Strategy.COOKIE,
28
+ browser: true,
29
+ navigateBefore: false,
30
+ args: [
31
+ { name: 'from', required: true, positional: true, help: 'Departure city (e.g. London / Paris / Shanghai)' },
32
+ { name: 'to', required: true, positional: true, help: 'Arrival city (e.g. Manchester / Lyon / Beijing)' },
33
+ { name: 'country', required: true, help: 'Route country slug (e.g. uk / france / italy / spain / germany / china)' },
34
+ { name: 'limit', type: 'int', default: 20, help: 'Number of journeys (1-50)' },
35
+ ],
36
+ columns: [
37
+ 'rank',
38
+ 'departureTime', 'fromStation',
39
+ 'arrivalTime', 'toStation',
40
+ 'duration', 'changes',
41
+ 'url',
42
+ ],
43
+ func: async (page, kwargs) => {
44
+ const from = parseKeyword('from', kwargs.from);
45
+ const to = parseKeyword('to', kwargs.to);
46
+ const country = parseKeyword('country', kwargs.country);
47
+ const limit = parseListLimit(kwargs.limit);
48
+
49
+ const searchUrl = buildTrainRouteUrl(country, from, to);
50
+ await page.goto(searchUrl);
51
+ const waitResult = await page.evaluate(WAIT_FOR_TRAINS_JS);
52
+ if (waitResult === 'captcha') {
53
+ throw new AuthRequiredError('trip.com', 'Trip.com is asking for a verification; complete it in your browser session and retry');
54
+ }
55
+ if (waitResult !== 'content') {
56
+ throw new CommandExecutionError(`Trip.com train timetable did not render (state=${String(waitResult)}); check the city names and --country`);
57
+ }
58
+ const raw = await page.evaluate(buildTrainExtractJs());
59
+ if (!Array.isArray(raw)) {
60
+ throw new CommandExecutionError('Trip.com train DOM extraction returned malformed rows');
61
+ }
62
+ if (raw.length === 0) {
63
+ throw new EmptyResultError('trip train', `No timetable for ${from} to ${to} (${country})`);
64
+ }
65
+ return raw.slice(0, limit).map((r, i) => ({
66
+ rank: i + 1,
67
+ departureTime: r.departureTime,
68
+ fromStation: r.fromStation,
69
+ arrivalTime: r.arrivalTime,
70
+ toStation: r.toStation,
71
+ duration: r.duration,
72
+ changes: r.changes,
73
+ url: searchUrl,
74
+ }));
75
+ },
76
+ });