fortrip 0.3.0 → 1.0.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/LICENSE +21 -0
- package/README.md +185 -74
- package/dist/chunk-OBK2KTLC.js +16333 -0
- package/dist/cli.js +253 -570
- package/dist/mcp-bridge-VEJ33LYD.js +191 -0
- package/package.json +18 -14
- package/AGENT_GUIDE.md +0 -133
- package/guides/planning.md +0 -54
- package/skills/fortrip-hotels/SKILL.md +0 -20
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
API,
|
|
4
|
+
CliError,
|
|
5
|
+
EXIT,
|
|
6
|
+
refreshSession,
|
|
7
|
+
requireToken
|
|
8
|
+
} from "./chunk-OBK2KTLC.js";
|
|
9
|
+
|
|
10
|
+
// src/mcp-bridge.ts
|
|
11
|
+
import {
|
|
12
|
+
isJSONRPCErrorResponse,
|
|
13
|
+
isJSONRPCRequest,
|
|
14
|
+
isJSONRPCResultResponse,
|
|
15
|
+
SdkHttpError,
|
|
16
|
+
StreamableHTTPClientTransport
|
|
17
|
+
} from "@modelcontextprotocol/client";
|
|
18
|
+
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
19
|
+
var SIGN_IN_REQUIRED = -32001;
|
|
20
|
+
var INTERNAL_ERROR = -32603;
|
|
21
|
+
var LOGIN_COMMAND = "npx -y fortrip login";
|
|
22
|
+
var SignInRequired = class extends Error {
|
|
23
|
+
constructor(message, data = {}) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.data = data;
|
|
26
|
+
}
|
|
27
|
+
data;
|
|
28
|
+
};
|
|
29
|
+
var signInAdvice = (link = "") => `Run \`${LOGIN_COMMAND}\` in a terminal${link}, then try again.`;
|
|
30
|
+
async function signedIn(run) {
|
|
31
|
+
try {
|
|
32
|
+
return await run();
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error instanceof CliError && error.exitCode === EXIT.unauthenticated) {
|
|
35
|
+
if (error.details.tokenSource === "FORTRIP_TOKEN") throw new SignInRequired(error.message);
|
|
36
|
+
const { loginUrl, userCode, expiresInSeconds } = error.details;
|
|
37
|
+
throw new SignInRequired(
|
|
38
|
+
`Not signed in to Fortrip, or the saved sign-in no longer works. ${signInAdvice(
|
|
39
|
+
loginUrl ? `, or open ${loginUrl} and approve` : ""
|
|
40
|
+
)}`,
|
|
41
|
+
{ loginUrl, userCode, expiresInSeconds }
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function authorizedFetch(apiUrl) {
|
|
48
|
+
const attempt = async (url, init) => {
|
|
49
|
+
try {
|
|
50
|
+
return await fetch(url, init);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error?.name === "AbortError") throw error;
|
|
53
|
+
const cause = error?.cause ?? error;
|
|
54
|
+
throw new Error(`Could not reach ${apiUrl}: ${cause?.message ?? String(error)}`);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
let turn = Promise.resolve();
|
|
58
|
+
return async (url, init) => {
|
|
59
|
+
const withToken = (token2) => {
|
|
60
|
+
const headers = new Headers(init?.headers);
|
|
61
|
+
headers.set("authorization", `Bearer ${token2}`);
|
|
62
|
+
return attempt(url, { ...init, headers });
|
|
63
|
+
};
|
|
64
|
+
const started = turn.then(async () => {
|
|
65
|
+
const { token: token2, source: source2 } = await signedIn(() => requireToken(apiUrl));
|
|
66
|
+
return { token: token2, source: source2, response: withToken(token2) };
|
|
67
|
+
});
|
|
68
|
+
turn = started.catch(() => {
|
|
69
|
+
});
|
|
70
|
+
const { token, source, response } = await started;
|
|
71
|
+
const first = await response;
|
|
72
|
+
if (first.status !== 401) return first;
|
|
73
|
+
await first.body?.cancel().catch(() => {
|
|
74
|
+
});
|
|
75
|
+
if (source === "env") {
|
|
76
|
+
throw new SignInRequired(
|
|
77
|
+
`Fortrip didn't accept FORTRIP_TOKEN. Replace it, or unset it and run \`${LOGIN_COMMAND}\`, then try again.`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
const second = await withToken(await signedIn(() => refreshSession(apiUrl, token)));
|
|
81
|
+
if (second.status !== 401) return second;
|
|
82
|
+
await second.body?.cancel().catch(() => {
|
|
83
|
+
});
|
|
84
|
+
throw new SignInRequired(`Fortrip didn't accept the refreshed sign-in. ${signInAdvice()}`);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function isRecord(value) {
|
|
88
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
89
|
+
}
|
|
90
|
+
function rpcErrorOf(error) {
|
|
91
|
+
if (error instanceof SignInRequired) {
|
|
92
|
+
return { code: SIGN_IN_REQUIRED, message: error.message, data: error.data };
|
|
93
|
+
}
|
|
94
|
+
if (error instanceof SdkHttpError) {
|
|
95
|
+
const { status } = error;
|
|
96
|
+
const text = typeof error.data.text === "string" ? error.data.text : "";
|
|
97
|
+
let body;
|
|
98
|
+
try {
|
|
99
|
+
body = JSON.parse(text);
|
|
100
|
+
} catch {
|
|
101
|
+
}
|
|
102
|
+
if (isRecord(body) && isRecord(body.error)) {
|
|
103
|
+
const { code, message } = body.error;
|
|
104
|
+
if (typeof code === "number" && typeof message === "string") return body.error;
|
|
105
|
+
}
|
|
106
|
+
const said = isRecord(body) ? body.error ?? body.message : text.slice(0, 200);
|
|
107
|
+
return {
|
|
108
|
+
code: INTERNAL_ERROR,
|
|
109
|
+
message: `Fortrip answered HTTP ${status}${typeof said === "string" && said ? `: ${said}` : ""}`,
|
|
110
|
+
data: { status, ...status >= 500 || status === 429 ? { retryable: true } : {} }
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
code: INTERNAL_ERROR,
|
|
115
|
+
message: error instanceof Error ? error.message : String(error),
|
|
116
|
+
...error instanceof CliError && error.details.retryable ? { data: { retryable: true } } : {}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
var CONNECTION_LOST = {
|
|
120
|
+
code: INTERNAL_ERROR,
|
|
121
|
+
message: "The connection to Fortrip ended before it answered.",
|
|
122
|
+
data: { retryable: true }
|
|
123
|
+
};
|
|
124
|
+
async function runMcpBridge(apiUrl, io = {
|
|
125
|
+
stdin: process.stdin,
|
|
126
|
+
stdout: process.stdout
|
|
127
|
+
}) {
|
|
128
|
+
const stderr = io.stderr ?? process.stderr;
|
|
129
|
+
const stdio = new StdioServerTransport(io.stdin, io.stdout);
|
|
130
|
+
const http = new StreamableHTTPClientTransport(new URL(API.mcp, apiUrl), {
|
|
131
|
+
fetch: authorizedFetch(apiUrl)
|
|
132
|
+
});
|
|
133
|
+
let closed = false;
|
|
134
|
+
const pending = /* @__PURE__ */ new Set();
|
|
135
|
+
let initializeId;
|
|
136
|
+
const log = (message) => {
|
|
137
|
+
if (!closed) stderr.write(`fortrip mcp: ${message}
|
|
138
|
+
`);
|
|
139
|
+
};
|
|
140
|
+
const fail = (id, error) => {
|
|
141
|
+
if (closed || !pending.delete(id)) return;
|
|
142
|
+
stdio.send({ jsonrpc: "2.0", id, error }).catch((cause) => log(cause.message));
|
|
143
|
+
};
|
|
144
|
+
stdio.onmessage = async (message) => {
|
|
145
|
+
if (!isJSONRPCRequest(message)) {
|
|
146
|
+
await http.send(message).catch(() => {
|
|
147
|
+
});
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
pending.add(message.id);
|
|
151
|
+
if (message.method === "initialize") initializeId = message.id;
|
|
152
|
+
try {
|
|
153
|
+
await http.send(message, { onRequestStreamEnd: () => fail(message.id, CONNECTION_LOST) });
|
|
154
|
+
} catch (error) {
|
|
155
|
+
fail(message.id, rpcErrorOf(error));
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
http.onmessage = (message) => {
|
|
159
|
+
if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
|
|
160
|
+
if (message.id !== void 0) pending.delete(message.id);
|
|
161
|
+
if (isJSONRPCResultResponse(message) && message.id === initializeId) {
|
|
162
|
+
const { protocolVersion } = message.result;
|
|
163
|
+
if (typeof protocolVersion === "string") http.setProtocolVersion(protocolVersion);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
stdio.send(message).catch((cause) => log(cause.message));
|
|
167
|
+
};
|
|
168
|
+
http.onerror = (error) => log(error.message);
|
|
169
|
+
stdio.onerror = (error) => log(error.message);
|
|
170
|
+
let stopped;
|
|
171
|
+
const done = new Promise((resolve) => {
|
|
172
|
+
stopped = resolve;
|
|
173
|
+
});
|
|
174
|
+
const shutdown = async () => {
|
|
175
|
+
if (closed) return;
|
|
176
|
+
closed = true;
|
|
177
|
+
process.off("SIGINT", shutdown);
|
|
178
|
+
process.off("SIGTERM", shutdown);
|
|
179
|
+
await Promise.allSettled([http.close(), stdio.close()]);
|
|
180
|
+
stopped();
|
|
181
|
+
};
|
|
182
|
+
stdio.onclose = shutdown;
|
|
183
|
+
process.on("SIGINT", shutdown);
|
|
184
|
+
process.on("SIGTERM", shutdown);
|
|
185
|
+
await http.start();
|
|
186
|
+
await stdio.start();
|
|
187
|
+
return done;
|
|
188
|
+
}
|
|
189
|
+
export {
|
|
190
|
+
runMcpBridge
|
|
191
|
+
};
|
package/package.json
CHANGED
|
@@ -1,37 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fortrip",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "MIT",
|
|
4
5
|
"description": "Search, price and book Fortrip hotels from the terminal or an AI agent.",
|
|
6
|
+
"mcpName": "io.github.FortripEngineering/fortrip",
|
|
5
7
|
"type": "module",
|
|
6
8
|
"repository": {
|
|
7
9
|
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/FortripEngineering/fortrip
|
|
10
|
+
"url": "git+https://github.com/FortripEngineering/fortrip.git",
|
|
11
|
+
"directory": "packages/cli"
|
|
9
12
|
},
|
|
10
13
|
"bin": {
|
|
11
14
|
"fortrip": "dist/cli.js"
|
|
12
15
|
},
|
|
13
16
|
"files": [
|
|
14
17
|
"dist",
|
|
15
|
-
"skills",
|
|
16
|
-
"guides",
|
|
17
|
-
"AGENT_GUIDE.md",
|
|
18
18
|
"README.md"
|
|
19
19
|
],
|
|
20
20
|
"engines": {
|
|
21
21
|
"node": ">=22"
|
|
22
22
|
},
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"typecheck": "tsc --noEmit",
|
|
27
|
-
"test": "node --import tsx --test test/*.test.ts",
|
|
28
|
-
"prepublishOnly": "pnpm typecheck && pnpm test && pnpm build"
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@modelcontextprotocol/client": "2.3.1",
|
|
25
|
+
"@modelcontextprotocol/server": "2.3.1"
|
|
29
26
|
},
|
|
30
27
|
"devDependencies": {
|
|
31
28
|
"@types/node": "^22.15.0",
|
|
32
29
|
"tsup": "^8.5.1",
|
|
33
30
|
"tsx": "^4.20.0",
|
|
34
|
-
"typescript": "^5.9.0"
|
|
31
|
+
"typescript": "^5.9.0",
|
|
32
|
+
"zod": "4.4.3",
|
|
33
|
+
"@fortrip/agent-contract": "1.0.0"
|
|
35
34
|
},
|
|
36
|
-
"
|
|
37
|
-
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup",
|
|
37
|
+
"dev": "tsx src/cli.ts",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"test": "node --import tsx --test test/*.test.ts"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/AGENT_GUIDE.md
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
# Fortrip hotels
|
|
2
|
-
|
|
3
|
-
Fortrip sells hotel rooms at supplier net prices. The
|
|
4
|
-
`fortrip` CLI (npm package `fortrip`) calls the Fortrip API as the
|
|
5
|
-
signed-in user and prints JSON.
|
|
6
|
-
|
|
7
|
-
## When to use
|
|
8
|
-
|
|
9
|
-
- The user wants hotels in a city, near a point, or by name, for given dates.
|
|
10
|
-
- The user wants live rooms, meal plans, cancellation terms or prices for
|
|
11
|
-
specific hotels.
|
|
12
|
-
- The user has picked a rate and wants to book it (you return a payment link;
|
|
13
|
-
the user pays in their browser).
|
|
14
|
-
|
|
15
|
-
Not for flights, activities, or changing/cancelling an existing booking.
|
|
16
|
-
|
|
17
|
-
For flexible dates, a budget, split stays, several destinations or "find me
|
|
18
|
-
the best", also run `npx -y fortrip@latest skill planning` before searching.
|
|
19
|
-
|
|
20
|
-
## Setup
|
|
21
|
-
|
|
22
|
-
Run every command as `npx -y fortrip@latest <command>`, the same version that
|
|
23
|
-
printed this guide. The examples below shorten that to `fortrip`.
|
|
24
|
-
|
|
25
|
-
When the user first asks about hotels, run `fortrip whoami` before asking
|
|
26
|
-
follow-up questions, so a login can happen while they answer.
|
|
27
|
-
|
|
28
|
-
If a command exits with code **3** and stderr has `loginUrl`, show it to the
|
|
29
|
-
user as a link together with `userCode`, e.g. "Sign in to Fortrip: <loginUrl>
|
|
30
|
-
(code BCDF-GHJK) and tell me when you're done." When they confirm, rerun the
|
|
31
|
-
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.
|
|
40
|
-
|
|
41
|
-
## Exit codes
|
|
42
|
-
|
|
43
|
-
| Code | Meaning | What to do |
|
|
44
|
-
|---|---|---|
|
|
45
|
-
| 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 |
|
|
48
|
-
| 3 | Not logged in | Show `loginUrl` to the user, then rerun (above) |
|
|
49
|
-
| 4 | Rate limited | Wait `retryAfterSeconds`, then retry once |
|
|
50
|
-
|
|
51
|
-
## Recommended order
|
|
52
|
-
|
|
53
|
-
1. **Search** — find candidates and indicative prices:
|
|
54
|
-
|
|
55
|
-
```bash
|
|
56
|
-
fortrip search --query "Bangkok" --check-in 2026-11-10 --check-out 2026-11-12 \
|
|
57
|
-
--star-min 4 --sort price-asc --page-size 10
|
|
58
|
-
```
|
|
59
|
-
|
|
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.
|
|
64
|
-
`--facility` names must match the catalogue exactly (e.g. `Swimming pool`,
|
|
65
|
-
`Parking`), otherwise nothing matches. Prices here (`price.nightlyFrom`,
|
|
66
|
-
`totalFrom`) are **indicative floors**, not quotes — say so.
|
|
67
|
-
|
|
68
|
-
2. **Hotel detail** (optional) — description, policies, facilities, photos:
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
fortrip hotel 266000 [--with-rooms]
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
3. **Rates** — live rooms and prices for up to 15 hotels:
|
|
75
|
-
|
|
76
|
-
```bash
|
|
77
|
-
fortrip rates --hotel 266000 --hotel 266001 --check-in 2026-11-10 \
|
|
78
|
-
--check-out 2026-11-12 --rooms 1 --adults 2 [--child-age 6] \
|
|
79
|
-
[--nationality CN] [--board BB] [--free-cancellation] [--currency USD]
|
|
80
|
-
```
|
|
81
|
-
|
|
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,
|
|
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).
|
|
89
|
-
|
|
90
|
-
4. **Book** — only after the user picks one rate and gives guest details:
|
|
91
|
-
|
|
92
|
-
```bash
|
|
93
|
-
fortrip book --hotel 266000 --room-id 9191658 --rate-plan "<ratePlanCode>" \
|
|
94
|
-
--check-in 2026-11-10 --check-out 2026-11-12 --rooms 1 --adults 2 \
|
|
95
|
-
--guest "John Smith" --contact-name "John Smith" \
|
|
96
|
-
--contact-phone "+8613800000000" [--contact-email john@example.com]
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
`--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.
|
|
104
|
-
|
|
105
|
-
## Before giving the payment link
|
|
106
|
-
|
|
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`.
|
|
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.
|
|
119
|
-
3. The link is for **this user only**: whoever has it can pay. Don't post it
|
|
120
|
-
in shared channels, logs or tickets. It expires after about 3 hours 45
|
|
121
|
-
minutes; call `book` again with the same details for a fresh one.
|
|
122
|
-
|
|
123
|
-
After paying, the user lands on `statusUrl`, which shows confirmation progress
|
|
124
|
-
without signing in. `orderUrl` is the signed-in order page.
|
|
125
|
-
|
|
126
|
-
## Errors from `book`
|
|
127
|
-
|
|
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`.
|
package/guides/planning.md
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
# Planning a hotel stay
|
|
2
|
-
|
|
3
|
-
Read this when the request is more than one hotel for fixed dates: flexible
|
|
4
|
-
dates, a budget, split stays, several destinations, or "find me the best".
|
|
5
|
-
Commands, login and the booking checklist are in `fortrip skill`; this covers
|
|
6
|
-
what to ask, how to compare and how to present. As there, `fortrip` is short
|
|
7
|
-
for `npx -y fortrip@latest`.
|
|
8
|
-
|
|
9
|
-
## What to collect
|
|
10
|
-
|
|
11
|
-
Keep track of: dates (or the flexible window), nights, destination(s), guests
|
|
12
|
-
and rooms, room constraints (beds, connecting rooms, accessibility), budget,
|
|
13
|
-
and what the user said matters most (cheapest, most flexible, closest to X).
|
|
14
|
-
|
|
15
|
-
Ask only for what changes which hotels qualify or how they rank. If children
|
|
16
|
-
come along, ask each child's age; `rates` and `book` need it. Don't ask for
|
|
17
|
-
trip type or style: infer it from the conversation or leave it general.
|
|
18
|
-
|
|
19
|
-
## Evidence
|
|
20
|
-
|
|
21
|
-
- A `search` price is a floor. Never call a search result available or
|
|
22
|
-
bookable.
|
|
23
|
-
- Use `hotel` only when a stated preference needs evidence (a pool, a
|
|
24
|
-
policy, room size).
|
|
25
|
-
- A hotel is available only once `rates` returned a live option for it.
|
|
26
|
-
- Anything the CLI didn't return, such as reviews, style, price trends or
|
|
27
|
-
exchange rates, is unknown. Say so instead of filling it in.
|
|
28
|
-
|
|
29
|
-
## Ranking
|
|
30
|
-
|
|
31
|
-
1. Drop options that break a hard constraint (dates, guests, budget cap,
|
|
32
|
-
required facility).
|
|
33
|
-
2. Keep only options with a live rate.
|
|
34
|
-
3. Order by the user's stated main goal. Goals they didn't state don't count.
|
|
35
|
-
4. Use other preferences only to break ties.
|
|
36
|
-
|
|
37
|
-
Use the cheapest comparable live room as the baseline unless the user fixed
|
|
38
|
-
the room type. Show at most three plans, each with its price difference from
|
|
39
|
-
the baseline and what it trades off (meal plan, cancellation, location,
|
|
40
|
-
instant confirmation). Keep hotel names as returned and dates in the user's
|
|
41
|
-
notation.
|
|
42
|
-
|
|
43
|
-
For a split stay or several destinations, price each leg with its own
|
|
44
|
-
`rates` call and add up the legs. Each leg is a separate `book` call and a
|
|
45
|
-
separate payment link.
|
|
46
|
-
|
|
47
|
-
## Stop and say so
|
|
48
|
-
|
|
49
|
-
- `rates` covers up to 15 hotels per call and takes about 15 seconds. If the
|
|
50
|
-
request needs more hotels or date combinations than a few calls can cover,
|
|
51
|
-
narrow it with the user, or present the result as a comparison of the
|
|
52
|
-
hotels you priced, not the best overall.
|
|
53
|
-
- Urgency is not consent: create a payment link only after the user picked a
|
|
54
|
-
rate and confirmed its terms.
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: fortrip-hotels
|
|
3
|
-
description: Search hotels, compare live room rates and create a hotel booking with a payment link through Fortrip. Use when the user wants to find, compare, price or book a hotel stay, including flexible dates, budgets, split stays or several destinations, or asks for a hotel payment link.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Fortrip hotels
|
|
7
|
-
|
|
8
|
-
Before anything else, run:
|
|
9
|
-
|
|
10
|
-
```bash
|
|
11
|
-
npx -y fortrip@latest skill
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
It prints the current instructions for the Fortrip CLI: commands, call order,
|
|
15
|
-
login, and what to show the user before giving a payment link. Follow them.
|
|
16
|
-
They ship with each CLI release, so this file stays short and doesn't go out
|
|
17
|
-
of date.
|
|
18
|
-
|
|
19
|
-
Needs Node.js 22 or newer. If the command fails, tell the user why instead of
|
|
20
|
-
guessing the CLI's flags.
|