fortrip 0.2.1 → 0.3.0

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/AGENT_GUIDE.md CHANGED
@@ -58,9 +58,12 @@ token into the chat.
58
58
  ```
59
59
 
60
60
  Use `--near lat,lng,radiusKm` for "near X", `--city-code` when known.
61
+ Pass `--currency` (`CNY`, the default, or `USD`) to suit the user, inferred
62
+ from their language or location, on both `search` and `rates`;
63
+ `--price-min` / `--price-max` are in that currency.
61
64
  `--facility` names must match the catalogue exactly (e.g. `Swimming pool`,
62
65
  `Parking`), otherwise nothing matches. Prices here (`price.nightlyFrom`,
63
- `totalFrom`, CNY) are **indicative floors**, not quotes — say so.
66
+ `totalFrom`) are **indicative floors**, not quotes — say so.
64
67
 
65
68
  2. **Hotel detail** (optional) — description, policies, facilities, photos:
66
69
 
@@ -73,11 +76,13 @@ token into the chat.
73
76
  ```bash
74
77
  fortrip rates --hotel 266000 --hotel 266001 --check-in 2026-11-10 \
75
78
  --check-out 2026-11-12 --rooms 1 --adults 2 [--child-age 6] \
76
- [--nationality CN] [--board BB] [--free-cancellation]
79
+ [--nationality CN] [--board BB] [--free-cancellation] [--currency USD]
77
80
  ```
78
81
 
79
- This call takes about 15 seconds. `totalPrice` is CNY at Stripe's exchange
80
- rate (refreshed hourly). Present each option with meal plan (`boardCode`: RO room only,
82
+ This call takes about 15 seconds. Prices are in the response's `currency`,
83
+ converted at Stripe's exchange rate (refreshed hourly), or a fixed fallback
84
+ rate when Stripe is unavailable; `fx` says which, and is `null` when
85
+ nothing needed converting. Present each option with meal plan (`boardCode`: RO room only,
81
86
  BB breakfast, HB half board, FB full board, AI all inclusive),
82
87
  `freeCancelUntil` / `refundable`, `isInstantConfirm`, and any `fees`
83
88
  (taxes paid at the hotel, in their own currency).
@@ -106,11 +111,11 @@ token into the chat.
106
111
  (`boardCode`, `boardCount`), cancellation (`refundable`, `freeCancelUntil`,
107
112
  the `cancellation` ladder), confirmation (`isInstantConfirm`,
108
113
  `confirmByMins`) and `payAtHotelFees`.
109
- 2. **Explain the amount.** `rates` showed CNY, but the charge is `amount` in
110
- `currency` (usually USD), and the Stripe page may display the user's local
111
- currency at Stripe's rate. The CNY figure used a Stripe rate too, so it is
112
- usually close, but the amount on the payment page is what will be charged;
113
- the CNY figure was a reference.
114
+ 2. **Explain the amount.** `rates` showed a converted figure, but the charge
115
+ is `amount` in `currency` (usually USD), and the Stripe page may display
116
+ the user's local currency at Stripe's rate. The `rates` figure used a
117
+ Stripe rate too, so it is usually close, but the amount on the payment page
118
+ is what will be charged; the `rates` figure was a reference.
114
119
  3. The link is for **this user only**: whoever has it can pay. Don't post it
115
120
  in shared channels, logs or tickets. It expires after about 3 hours 45
116
121
  minutes; call `book` again with the same details for a fresh one.
package/README.md CHANGED
@@ -48,6 +48,9 @@ Output is JSON on stdout. Errors are JSON on stderr, with fixed exit codes:
48
48
  - `FORTRIP_TOKEN` overrides the saved login (CI, remote sandboxes).
49
49
  - `FORTRIP_API_URL` overrides the base URL (default `https://fortrip.ai`).
50
50
  Plain `http` is only accepted for `localhost`.
51
+ - `FORTRIP_CURRENCY` sets the default `--currency` for `search` and `rates`
52
+ (`CNY` or `USD`; the API defaults to `CNY`). Display only: `book`
53
+ charges the currency it returns.
51
54
 
52
55
  Tokens don't expire; `logout` revokes them.
53
56
 
package/dist/cli.js CHANGED
@@ -102,6 +102,7 @@ function unwrap(response) {
102
102
  import { readFileSync as readFileSync2 } from "fs";
103
103
  import { parseArgs } from "util";
104
104
  var BODY_OPTION = { body: { type: "string" } };
105
+ var CURRENCY_OPTION = { currency: { type: "string" } };
105
106
  function parse(argv, options) {
106
107
  try {
107
108
  return parseArgs({ args: argv, options, allowPositionals: true, strict: true });
@@ -147,6 +148,13 @@ function str(value) {
147
148
  function list(value) {
148
149
  return Array.isArray(value) && value.length ? value : void 0;
149
150
  }
151
+ function currency(values, base, env) {
152
+ const flag = str(values.currency)?.trim().toUpperCase();
153
+ if (flag === "") throw usageError("--currency must not be empty");
154
+ if (flag) return flag;
155
+ if (base.currency !== void 0) return void 0;
156
+ return env.FORTRIP_CURRENCY?.trim().toUpperCase() || void 0;
157
+ }
150
158
  function merge(base, overrides) {
151
159
  const body = { ...base };
152
160
  for (const [key, value] of Object.entries(overrides)) {
@@ -156,6 +164,7 @@ function merge(base, overrides) {
156
164
  }
157
165
  var SEARCH_OPTIONS = {
158
166
  ...BODY_OPTION,
167
+ ...CURRENCY_OPTION,
159
168
  query: { type: "string", short: "q" },
160
169
  "city-code": { type: "string" },
161
170
  near: { type: "string" },
@@ -170,7 +179,8 @@ var SEARCH_OPTIONS = {
170
179
  page: { type: "string" },
171
180
  "page-size": { type: "string" }
172
181
  };
173
- function searchBody(values) {
182
+ function searchBody(values, env = process.env) {
183
+ const base = baseBody(values);
174
184
  let near;
175
185
  const nearRaw = str(values.near);
176
186
  if (nearRaw !== void 0) {
@@ -184,7 +194,7 @@ function searchBody(values) {
184
194
  radiusKm: num("near", radiusKm)
185
195
  };
186
196
  }
187
- return merge(baseBody(values), {
197
+ return merge(base, {
188
198
  query: str(values.query),
189
199
  cityCode: num("city-code", str(values["city-code"])),
190
200
  near,
@@ -197,7 +207,8 @@ function searchBody(values) {
197
207
  priceMax: num("price-max", str(values["price-max"])),
198
208
  sortBy: str(values.sort),
199
209
  page: num("page", str(values.page)),
200
- pageSize: num("page-size", str(values["page-size"]))
210
+ pageSize: num("page-size", str(values["page-size"])),
211
+ currency: currency(values, base, env)
201
212
  });
202
213
  }
203
214
  var HOTEL_OPTIONS = {
@@ -228,6 +239,7 @@ function stayFields(values, base) {
228
239
  }
229
240
  var RATES_OPTIONS = {
230
241
  ...BODY_OPTION,
242
+ ...CURRENCY_OPTION,
231
243
  ...STAY_OPTIONS,
232
244
  hotel: { type: "string", multiple: true },
233
245
  nationality: { type: "string" },
@@ -236,7 +248,7 @@ var RATES_OPTIONS = {
236
248
  "include-unbookable": { type: "boolean" },
237
249
  "max-rooms": { type: "string" }
238
250
  };
239
- function ratesBody(values) {
251
+ function ratesBody(values, env = process.env) {
240
252
  const base = baseBody(values);
241
253
  return merge(base, {
242
254
  hotelIds: nums("hotel", list(values.hotel)),
@@ -247,7 +259,8 @@ function ratesBody(values) {
247
259
  ),
248
260
  freeCancellation: values["free-cancellation"] ? true : void 0,
249
261
  bookableOnly: values["include-unbookable"] ? false : void 0,
250
- maxRoomsPerHotel: num("max-rooms", str(values["max-rooms"]))
262
+ maxRoomsPerHotel: num("max-rooms", str(values["max-rooms"])),
263
+ currency: currency(values, base, env)
251
264
  });
252
265
  }
253
266
  var BOOK_OPTIONS = {
@@ -538,9 +551,9 @@ Commands:
538
551
  login [--no-browser] Sign in through the browser (device code)
539
552
  logout Revoke and forget the saved token
540
553
  whoami Show the signed-in account
541
- search Find hotels with indicative prices (CNY)
554
+ search Find hotels with indicative prices
542
555
  hotel <hotelId> Static hotel detail [--with-rooms]
543
- rates Live rooms and rates (CNY) for up to 15 hotels
556
+ rates Live rooms and rates for up to 15 hotels
544
557
  book Create an order and get a payment link
545
558
  skill [planning] Print the guide for AI agents (Markdown)
546
559
 
@@ -548,10 +561,11 @@ search: --query <text> | --city-code <n> | --near <lat,lng,km>
548
561
  --check-in YYYY-MM-DD --check-out YYYY-MM-DD
549
562
  [--star-min n] [--star-max n] [--facility <name>]... [--price-min n]
550
563
  [--price-max n] [--sort recommended|price-asc|price-desc|star-desc|distance]
551
- [--page n] [--page-size n]
564
+ [--page n] [--page-size n] [--currency CNY|USD]
552
565
  rates: --hotel <id>... --check-in --check-out [--rooms 1] [--adults 2]
553
566
  [--child-age n]... [--nationality CN] [--board BB]...
554
567
  [--free-cancellation] [--include-unbookable] [--max-rooms n]
568
+ [--currency CNY|USD]
555
569
  book: --hotel <id> --room-id <id> --rate-plan <code> --check-in --check-out
556
570
  [--rooms 1] [--adults 2] [--child-age n]...
557
571
  --guest "First Last[:roomNo]"... --contact-name <name>
@@ -561,12 +575,18 @@ book: --hotel <id> --room-id <id> --rate-plan <code> --check-in --check-out
561
575
  search, rates and book also take --body '<json>' (or --body @file.json) with
562
576
  the raw request body; flags override its fields.
563
577
 
578
+ search and rates ask for prices in --currency (CNY by default, or USD),
579
+ converted at Stripe's rate or a fixed fallback rate; the response's currency
580
+ and fx fields say which. book still charges the currency it returns (usually
581
+ USD).
582
+
564
583
  Output is JSON on stdout; errors are JSON on stderr.
565
584
  Exit codes: 0 ok, 1 error, 2 invalid arguments, 3 not logged in, 4 rate limited.
566
585
 
567
586
  Environment:
568
587
  FORTRIP_TOKEN Use this token instead of the saved login (CI, sandboxes)
569
588
  FORTRIP_API_URL API base URL (default https://fortrip.ai)
589
+ FORTRIP_CURRENCY Default --currency for search and rates
570
590
  `;
571
591
  async function run(argv) {
572
592
  const [command, ...rest] = argv;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fortrip",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Search, price and book Fortrip hotels from the terminal or an AI agent.",
5
5
  "type": "module",
6
6
  "repository": {