fortrip 0.3.0 → 0.3.1

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
@@ -1,4 +1,4 @@
1
- # Fortrip hotels
1
+ # Fortrip
2
2
 
3
3
  Fortrip sells hotel rooms at supplier net prices. The
4
4
  `fortrip` CLI (npm package `fortrip`) calls the Fortrip API as the
@@ -13,6 +13,9 @@ signed-in user and prints JSON.
13
13
  the user pays in their browser).
14
14
 
15
15
  Not for flights, activities, or changing/cancelling an existing booking.
16
+ For changes, cancellations, refunds or any other problem with a booking, the
17
+ user emails info@fortrip.ai with the booking's `orderUrl` (never
18
+ `paymentUrl` or `statusUrl`).
16
19
 
17
20
  For flexible dates, a budget, split stays, several destinations or "find me
18
21
  the best", also run `npx -y fortrip@latest skill planning` before searching.
@@ -20,7 +23,8 @@ the best", also run `npx -y fortrip@latest skill planning` before searching.
20
23
  ## Setup
21
24
 
22
25
  Run every command as `npx -y fortrip@latest <command>`, the same version that
23
- printed this guide. The examples below shorten that to `fortrip`.
26
+ printed this guide. The examples below shorten that to `fortrip`;
27
+ `fortrip <command> --help` lists a command's flags.
24
28
 
25
29
  When the user first asks about hotels, run `fortrip whoami` before asking
26
30
  follow-up questions, so a login can happen while they answer.
@@ -29,25 +33,35 @@ If a command exits with code **3** and stderr has `loginUrl`, show it to the
29
33
  user as a link together with `userCode`, e.g. "Sign in to Fortrip: <loginUrl>
30
34
  (code BCDF-GHJK) and tell me when you're done." When they confirm, rerun the
31
35
  same command; it finishes the login itself. Don't run `fortrip login` — it
32
- waits for up to 10 minutes. If the rerun gives a new `loginUrl`, the old one
33
- expired or was denied; show the new one.
34
-
35
- If exit 3 has no `loginUrl`, the saved token was revoked: run `fortrip logout`,
36
- then rerun the command to get a link.
37
-
38
- In CI, the user can set `FORTRIP_TOKEN` instead. Never ask the user to paste a
39
- token into the chat.
36
+ waits for up to 10 minutes. Commands that exit 3 together share one
37
+ `loginUrl`; show it once. If the rerun gives a different `loginUrl`, show
38
+ that one.
39
+
40
+ When Fortrip doesn't accept the saved sign-in (it was revoked or has
41
+ expired, or Fortrip had a passing problem), the command also exits 3 with a
42
+ `loginUrl`; handle it the same way. The saved sign-in is kept until the
43
+ user approves the new link, so after a passing problem the next command
44
+ works without signing in again. Exit 3 with `tokenSource: "FORTRIP_TOKEN"` means Fortrip
45
+ didn't accept the token the user set in `FORTRIP_TOKEN`: retry once a
46
+ little later, and if it keeps happening, tell the user to replace or unset
47
+ it.
48
+
49
+ Sign-in happens only in the browser. In CI, the user can set `FORTRIP_TOKEN`
50
+ instead. Never ask the user to paste a token into the chat.
40
51
 
41
52
  ## Exit codes
42
53
 
43
54
  | Code | Meaning | What to do |
44
55
  |---|---|---|
45
56
  | 0 | Success; JSON on stdout | Continue |
46
- | 1 | Other error (supplier error, rate no longer bookable, network) | Read `error` on stderr; re-run `rates` if a rate went stale |
47
- | 2 | Invalid arguments | Fix the flags; stderr `issues` lists the bad fields |
57
+ | 1 | Other error (supplier error, rate no longer bookable, network error or timeout) | Read `error` on stderr; if `retryable` is true (supplier unreachable or timed out, Fortrip briefly unavailable, or no response from Fortrip), wait `retryAfterSeconds` (or a few seconds) and retry once; re-run `rates` if a rate went stale. `book` errors carry a `reason` (below) |
58
+ | 2 | Invalid arguments | Fix the flags; stderr `issues` lists each bad field with a message |
48
59
  | 3 | Not logged in | Show `loginUrl` to the user, then rerun (above) |
49
60
  | 4 | Rate limited | Wait `retryAfterSeconds`, then retry once |
50
61
 
62
+ Output on stdout or stderr can carry `warning` (with `recoveryFile` when a
63
+ sign-in couldn't be saved): pass it on to the user.
64
+
51
65
  ## Recommended order
52
66
 
53
67
  1. **Search** — find candidates and indicative prices:
@@ -63,7 +77,9 @@ token into the chat.
63
77
  `--price-min` / `--price-max` are in that currency.
64
78
  `--facility` names must match the catalogue exactly (e.g. `Swimming pool`,
65
79
  `Parking`), otherwise nothing matches. Prices here (`price.nightlyFrom`,
66
- `totalFrom`) are **indicative floors**, not quotes — say so.
80
+ `totalFrom`) are **indicative floors**, not quotes — say so. A search
81
+ that matches too many hotels fails with `matched` and `maxMatches`: narrow
82
+ it (a city, an area, stars or a price range).
67
83
 
68
84
  2. **Hotel detail** (optional) — description, policies, facilities, photos:
69
85
 
@@ -79,13 +95,30 @@ token into the chat.
79
95
  [--nationality CN] [--board BB] [--free-cancellation] [--currency USD]
80
96
  ```
81
97
 
98
+ `--adults` and `--child-age` are **per room**, and every room has the same
99
+ occupancy: 2 rooms for 4 adults is `--rooms 2 --adults 2`. If the party
100
+ doesn't divide evenly (3 adults in 2 rooms), ask the user whether to price
101
+ every room for the larger number or book the rooms separately. Limits:
102
+ 1–8 rooms, 1–8 adults and 0–8 children (aged 0–17) per room, 1–15
103
+ nights, up to 15 hotels per call.
104
+
82
105
  This call takes about 15 seconds. Prices are in the response's `currency`,
83
106
  converted at Stripe's exchange rate (refreshed hourly), or a fixed fallback
84
107
  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,
86
- BB breakfast, HB half board, FB full board, AI all inclusive),
87
- `freeCancelUntil` / `refundable`, `isInstantConfirm`, and any `fees`
88
- (taxes paid at the hotel, in their own currency).
108
+ nothing needed converting. Present each option with:
109
+
110
+ - meal plan: `boardCode` (RO room only, BB breakfast, HB breakfast +
111
+ dinner, FB breakfast + lunch + dinner, BL lunch, BD dinner, HBBL
112
+ breakfast + lunch, HBLD lunch + dinner, AI all inclusive) together with
113
+ `boardCount`, how many guests per room the meal plan covers; fewer than
114
+ the adults per room means not everyone gets the meals (BB with
115
+ `boardCount` 1 for 2 adults is breakfast for 1 of 2), so say who is
116
+ covered;
117
+ - `freeCancelUntil` / `refundable`, `isInstantConfirm`, and any `fees`
118
+ (taxes paid at the hotel, in their own currency).
119
+
120
+ `--nationality` isn't sent with the booking: a rate restricted to that
121
+ nationality may change or be refused when booked.
89
122
 
90
123
  4. **Book** — only after the user picks one rate and gives guest details:
91
124
 
@@ -97,37 +130,71 @@ token into the chat.
97
130
  ```
98
131
 
99
132
  `--hotel`, `--room-id`, `--rate-plan`, dates, `--rooms`, `--adults` and
100
- `--child-age` must be exactly the values used in `rates`. Guest names must
101
- be in Latin letters as on the passport ("First Last"; last word is the last
102
- name; `"First Last:2"` puts the guest in room 2). Each room needs at least
103
- one guest. `--special-requests` must be in English and isn't guaranteed.
133
+ `--child-age` must be exactly the values used in `rates`; `book` requires
134
+ `--rooms` and `--adults`. Guest names must be in Latin letters as on the
135
+ passport ("First Last"; last word is the last name; `"First Last:2"` puts
136
+ the guest in room 2; 1–60 characters each). Each room needs at least one
137
+ guest. The contact name takes up to 120 characters, the phone 3–40.
138
+ `--special-requests` must be in English and isn't guaranteed. `book`
139
+ accepts only these fields: a `--body` with any other field (such as
140
+ `nationality`) is rejected with exit 2.
141
+
142
+ One booking covers at most 15 nights; book a longer stay as separate
143
+ bookings. `book` checks the rate with the supplier again and can take up
144
+ to about 2 minutes: allow the command at least 3 minutes, and if it times
145
+ out, run it again with identical flags. That is always safe: it returns
146
+ the same order, with the same or a refreshed payment link.
104
147
 
105
148
  ## Before giving the payment link
106
149
 
107
- `book` returns `paymentUrl`, `amount`, `currency`, `terms`, `statusUrl` and
108
- `orderUrl`. Before you hand over `paymentUrl`:
109
-
110
- 1. **Show the `terms`** and get the user's confirmation: meal plan
111
- (`boardCode`, `boardCount`), cancellation (`refundable`, `freeCancelUntil`,
112
- the `cancellation` ladder), confirmation (`isInstantConfirm`,
113
- `confirmByMins`) and `payAtHotelFees`.
150
+ `book` returns `paymentUrl`, `amount`, `currency`, `terms`, `stay`,
151
+ `statusUrl` and `orderUrl`. Before you hand over `paymentUrl`:
152
+
153
+ 1. **Show what will be booked and ask the user to confirm.** From `stay`:
154
+ hotel (`hotelName`), room (`roomName`), dates, nights, rooms and guests per
155
+ room (`adultsPerRoom`, `childrenAgesPerRoom`); if `stay` is missing, use
156
+ the values you passed. From `terms`: meal plan (`boardCode` with
157
+ `boardCount`), cancellation (`refundable`, `freeCancelUntil`, the
158
+ `cancellation` ladder), confirmation (`isInstantConfirm`, `confirmByMins`)
159
+ and `payAtHotelFees`. Then the `amount` and `currency`, and end your reply
160
+ by asking them to confirm. If `amount` or `terms` is `null`, say it
161
+ couldn't be read rather than guess; the payment page shows the amount.
114
162
  2. **Explain the amount.** `rates` showed a converted figure, but the charge
115
163
  is `amount` in `currency` (usually USD), and the Stripe page may display
116
164
  the user's local currency at Stripe's rate. The `rates` figure used a
117
165
  Stripe rate too, so it is usually close, but the amount on the payment page
118
166
  is what will be charged; the `rates` figure was a reference.
119
- 3. The link is for **this user only**: whoever has it can pay. Don't post it
167
+ 3. **Only after they confirm, give `paymentUrl` exactly as returned**: don't
168
+ shorten, rebuild or re-encode it. It opens a Fortrip page that recaps the
169
+ booking and continues to payment.
170
+ 4. The link is for **this user only**: whoever has it can pay. Don't post it
120
171
  in shared channels, logs or tickets. It expires after about 3 hours 45
121
172
  minutes; call `book` again with the same details for a fresh one.
122
173
 
174
+ Never ask for, accept or pass on payment card details; payment happens only
175
+ on the payment page. A card number in a guest name, the contact name or the
176
+ special requests is rejected with exit 2.
177
+
123
178
  After paying, the user lands on `statusUrl`, which shows confirmation progress
124
179
  without signing in. `orderUrl` is the signed-in order page.
125
180
 
126
181
  ## Errors from `book`
127
182
 
128
- - `409` with `agentOrderId`: this exact stay is already paid or completed —
129
- show `orderUrl`, don't book again.
130
- - `409` with `changes`: price or terms changed — re-run `rates` and confirm
131
- again with the user.
132
- - `422`: the booking was refused (guest names, too many attempts, bookings
133
- paused, or a preview deployment). Read `error`.
183
+ Act on `reason` in stderr:
184
+
185
+ - `already_booked` (409, with `agentOrderId`): this exact stay is already paid
186
+ or completed — show `orderUrl`, don't book again.
187
+ - `price_or_terms_changed` (409, with `changes`): show what changed, re-run
188
+ `rates` and confirm again with the user.
189
+ - `rate_unavailable` (422): the rate can no longer be booked — re-run `rates`
190
+ and offer what is left.
191
+ - `supplier_unavailable` (503 or 504, `retryable: true`): wait
192
+ `retryAfterSeconds` and retry once with identical flags. A 503 with
193
+ `retryable: true` and no `reason` (a passing outage) is handled the same
194
+ way.
195
+ - `refused` (any other 422): the booking was refused (guest names, too many
196
+ attempts, bookings paused, or a preview deployment). Read `error`.
197
+
198
+ Older servers send no `reason`: there a 409 with `agentOrderId` is
199
+ `already_booked`, a 409 with `changes` is `price_or_terms_changed`, any other
200
+ 409 is `rate_unavailable`, and a 422 is `refused`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 FortripEngineering
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,8 +1,12 @@
1
1
  # fortrip-cli
2
2
 
3
- Search, price and book Fortrip hotels from the terminal or an AI agent. The
3
+ Search, price and book Fortrip hotels from the terminal or an AI agent.
4
4
  The npm package and the command are both `fortrip`.
5
5
 
6
+ For MCP clients, see the separately maintained
7
+ [Fortrip MCP](https://github.com/FortripEngineering/fortrip-mcp)
8
+ repository for local and hosted connection instructions.
9
+
6
10
  ```bash
7
11
  npx fortrip login # sign in through the browser
8
12
  npx fortrip search --query Bangkok --check-in 2026-11-10 --check-out 2026-11-12
@@ -20,52 +24,77 @@ Requires Node.js 22 or newer. No runtime dependencies.
20
24
  | `search` | Hotels with indicative prices |
21
25
  | `hotel <hotelId> [--with-rooms]` | Static hotel detail |
22
26
  | `rates` | Live rooms and rates |
23
- | `book` | Order + Stripe payment link |
27
+ | `book` | Order + payment link (a Fortrip page that continues to Stripe) |
28
+
29
+ Run `fortrip --help` for every flag, or `fortrip <command> --help` for one
30
+ command's. `search`, `rates` and `book` also accept `--body '<json>'` or
31
+ `--body @file.json` with the raw request body; flags override its fields.
24
32
 
25
- Run `fortrip --help` for every flag. `search`, `rates` and `book` also accept
26
- `--body '<json>'` or `--body @file.json` with the raw request body; flags
27
- override its fields.
33
+ `--adults` and `--child-age` are per room, and every room has the same
34
+ occupancy: 2 rooms for 4 adults is `--rooms 2 --adults 2`. `rates` defaults
35
+ to 1 room for 2 adults; `book` requires `--rooms` and `--adults`.
28
36
 
29
37
  Output is JSON on stdout. Errors are JSON on stderr, with fixed exit codes:
30
38
 
31
39
  | Code | Meaning |
32
40
  |---|---|
33
41
  | 0 | Success |
34
- | 1 | Other error |
42
+ | 1 | Other error (`retryable: true` on stderr: worth one retry) |
35
43
  | 2 | Invalid arguments (local or HTTP 400) |
36
- | 3 | Not logged in / token revoked (HTTP 401) |
44
+ | 3 | Not logged in, or the sign-in wasn't accepted (HTTP 401) |
37
45
  | 4 | Rate limited (HTTP 429; `retryAfterSeconds` on stderr) |
38
46
 
39
47
  ## Credentials
40
48
 
41
49
  - `login` saves the token to `~/.config/fortrip/credentials.json`
42
- (`$XDG_CONFIG_HOME` is honoured), mode `0600`. Tokens are stored per API URL,
43
- so a token is only ever sent to the server that issued it.
50
+ (`$XDG_CONFIG_HOME` is honoured), mode `0600`. Saved tokens are stored per
51
+ API URL, so a saved token is only sent to the server that issued it.
44
52
  - Without a token, any other command starts a device login and exits 3 with
45
53
  `loginUrl` on stderr instead of waiting. Once that link is approved in the
46
54
  browser, rerunning the command saves the token and carries on. This is how
47
- AI agents sign in, since they can't sit in `login`.
48
- - `FORTRIP_TOKEN` overrides the saved login (CI, remote sandboxes).
55
+ AI agents sign in, since they can't sit in `login`. Commands run in
56
+ parallel share one login link. If the server doesn't accept the saved
57
+ token, the command keeps it and starts a new login the same way; once that
58
+ link is approved, the new token replaces the saved one.
59
+ - `FORTRIP_TOKEN` overrides the saved login (CI, remote sandboxes). It is not
60
+ tied to a server: it is sent to whatever `FORTRIP_API_URL` points at. If the
61
+ server doesn't accept it, commands exit 3 with `tokenSource: "FORTRIP_TOKEN"`
62
+ and no `loginUrl`.
49
63
  - `FORTRIP_API_URL` overrides the base URL (default `https://fortrip.ai`).
50
64
  Plain `http` is only accepted for `localhost`.
51
65
  - `FORTRIP_CURRENCY` sets the default `--currency` for `search` and `rates`
52
66
  (`CNY` or `USD`; the API defaults to `CNY`). Display only: `book`
53
67
  charges the currency it returns.
54
68
 
55
- Tokens don't expire; `logout` revokes them.
69
+ `logout` revokes the saved token. With the Fortrip server update released
70
+ alongside CLI 0.3.1, sign-ins that go unused for 90 days stop working, and
71
+ signed-in users can list and revoke their CLI sign-ins in their Fortrip
72
+ account (account menu → **Fortrip CLI**).
73
+
74
+ If a new sign-in can't be saved (the file is busy or damaged), it is kept
75
+ in a `credentials.recovered-*.json` file next to `credentials.json` (mode
76
+ `0600`), and the command's JSON output names it in `warning` and
77
+ `recoveryFile`. The next successful save merges it back and removes the
78
+ file; `logout` revokes and removes these too.
79
+
80
+ Sharing `credentials.json` with 0.3.0 works on a best-effort basis: 0.3.0
81
+ finds the newest pending login where it expects it, but it writes the file
82
+ without a lock, so don't sign in with 0.3.0 and 0.3.1 at the same moment.
56
83
 
57
84
  ## Agent skill
58
85
 
59
86
  ```bash
60
- npx skills add FortripEngineering/fortrip-cli
87
+ npx skills add FortripEngineering/fortrip-plugins --skill fortrip-cli
61
88
  ```
62
89
 
63
- [`skills/fortrip-hotels/SKILL.md`](skills/fortrip-hotels/SKILL.md) only tells
64
- the agent to run `npx -y fortrip@latest skill`, which prints
90
+ The skill lives in
91
+ [fortrip-plugins](https://github.com/FortripEngineering/fortrip-plugins); the
92
+ Claude directory plugin there uses the hosted MCP instead of the CLI. The
93
+ skill only tells the agent to run `npx -y fortrip@latest skill`, which prints
65
94
  [`AGENT_GUIDE.md`](AGENT_GUIDE.md): when to use the CLI, the call order, login,
66
95
  and what to show the user before handing over a payment link. The guide ships
67
96
  with the package, so a CLI release updates every installed skill. Edit
68
- `AGENT_GUIDE.md`, not `SKILL.md`.
97
+ `AGENT_GUIDE.md`, not the skill.
69
98
 
70
99
  `fortrip skill planning` prints [`guides/planning.md`](guides/planning.md):
71
100
  what to ask, how to rank and how to present options for flexible dates,
@@ -73,6 +102,12 @@ budgets, split stays and several destinations. Both guides are printed with
73
102
  `fortrip@latest` replaced by the running version, so an agent keeps using the
74
103
  same CLI for the whole conversation.
75
104
 
105
+ ## Support
106
+
107
+ For changes, cancellations, refunds or other help with a booking, email
108
+ [info@fortrip.ai](mailto:info@fortrip.ai) with the booking's order link
109
+ (`orderUrl`).
110
+
76
111
  ## Development
77
112
 
78
113
  ```bash
@@ -89,7 +124,9 @@ Point at a local Fortrip app with `FORTRIP_API_URL=http://localhost:3000`.
89
124
 
90
125
  Pushing a `v*` tag publishes to npm through
91
126
  [`.github/workflows/publish.yml`](.github/workflows/publish.yml), which signs
92
- in with npm trusted publishing (no npm token in GitHub).
127
+ in with npm trusted publishing (no npm token in GitHub). Pull requests and
128
+ pushes to `main` run typecheck, tests and the build through
129
+ [`.github/workflows/ci.yml`](.github/workflows/ci.yml).
93
130
 
94
131
  1. Bump the version in a PR and merge it into `main`:
95
132
  `npm version minor --no-git-tag-version` (or `patch`).
@@ -101,5 +138,13 @@ in with npm trusted publishing (no npm token in GitHub).
101
138
  ```
102
139
 
103
140
  The workflow refuses a tag that doesn't match `package.json` or isn't on
104
- `main`, then runs typecheck, tests and the build before publishing. Every
105
- installed skill picks up the new guide on its next `npx fortrip@latest skill`.
141
+ `main`. A `build` job runs typecheck, tests and the build and packs the
142
+ tarball; a `publish` job in the `npm-publish` environment publishes that
143
+ tarball, with no install or build of its own. Every installed skill picks up
144
+ the new guide on its next `npx fortrip@latest skill`.
145
+
146
+ ## License
147
+
148
+ The code and documentation in this repository are licensed under the
149
+ [MIT License](LICENSE). This license does not grant access to the hosted
150
+ Fortrip service, hotel inventory, or third-party data.