fortrip 2.0.0 → 3.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/README.md +131 -73
- package/dist/{chunk-53656LB2.js → chunk-26IHFJYT.js} +2223 -569
- package/dist/cli.js +262 -107
- package/dist/{mcp-bridge-LPQBHWLD.js → mcp-bridge-3WLWV2KY.js} +87 -24
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -4,20 +4,28 @@ import {
|
|
|
4
4
|
COMMANDS,
|
|
5
5
|
COMMAND_GROUPS,
|
|
6
6
|
CliError,
|
|
7
|
+
DEFAULT_API_URL,
|
|
8
|
+
DEFAULT_MCP_URL,
|
|
7
9
|
EXIT,
|
|
8
10
|
GLOBAL_FLAGS,
|
|
11
|
+
LIMITS,
|
|
12
|
+
TOOLS,
|
|
9
13
|
VERSION,
|
|
10
14
|
authorizedRequest,
|
|
11
15
|
commandInvocation,
|
|
16
|
+
fieldLines,
|
|
17
|
+
formatError,
|
|
12
18
|
login,
|
|
13
19
|
logout,
|
|
14
20
|
notices,
|
|
15
21
|
renderGuide,
|
|
22
|
+
renderToolResult,
|
|
16
23
|
resolveApiUrl,
|
|
24
|
+
resolveMcpUrl,
|
|
17
25
|
unwrap,
|
|
18
26
|
usageError,
|
|
19
27
|
whoami
|
|
20
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-26IHFJYT.js";
|
|
21
29
|
|
|
22
30
|
// src/commands.ts
|
|
23
31
|
import { readFileSync } from "fs";
|
|
@@ -25,7 +33,6 @@ import { parseArgs } from "util";
|
|
|
25
33
|
|
|
26
34
|
// src/help.ts
|
|
27
35
|
var WIDTH = 78;
|
|
28
|
-
var OPTIONAL_POSITIONALS = /* @__PURE__ */ new Set(["topic"]);
|
|
29
36
|
function wrap(text, indent) {
|
|
30
37
|
const pad = " ".repeat(indent);
|
|
31
38
|
const lines = [];
|
|
@@ -43,7 +50,7 @@ function wrap(text, indent) {
|
|
|
43
50
|
}
|
|
44
51
|
function positionals(spec) {
|
|
45
52
|
return (spec.positionals ?? []).map(
|
|
46
|
-
(name) =>
|
|
53
|
+
({ name, optional }) => optional ? `[${name}]` : `<${name}>`
|
|
47
54
|
);
|
|
48
55
|
}
|
|
49
56
|
function label(spec) {
|
|
@@ -53,13 +60,13 @@ function usageLine(spec) {
|
|
|
53
60
|
return [commandInvocation(spec), ...positionals(spec), "[options]"].join(" ");
|
|
54
61
|
}
|
|
55
62
|
function names(option, aligned) {
|
|
56
|
-
const short = option.short ? `-${option.short}, ` : aligned ? " " : "";
|
|
63
|
+
const short = "short" in option && option.short ? `-${option.short}, ` : aligned ? " " : "";
|
|
57
64
|
const placeholder = option.placeholder ? ` ${option.placeholder}` : "";
|
|
58
65
|
const repeats = "multiple" in option && option.multiple ? "..." : "";
|
|
59
66
|
return `${short}--${option.flag}${placeholder}${repeats}`;
|
|
60
67
|
}
|
|
61
68
|
function optionList(options) {
|
|
62
|
-
const aligned = options.some((option) => option.short);
|
|
69
|
+
const aligned = options.some((option) => "short" in option && option.short);
|
|
63
70
|
return options.map(
|
|
64
71
|
(option) => [` ${names(option, aligned)}`, ...wrap(option.description, 8)].join("\n")
|
|
65
72
|
).join("\n");
|
|
@@ -98,7 +105,7 @@ ${optionList(HELP_FLAG)}
|
|
|
98
105
|
`;
|
|
99
106
|
}
|
|
100
107
|
var CURRENCY_COMMANDS = COMMANDS.filter(
|
|
101
|
-
(spec) => spec.flags.some((flag) => flag.field === "
|
|
108
|
+
(spec) => spec.flags.some((flag) => flag.field === "displayCurrency")
|
|
102
109
|
).map((spec) => commandInvocation(spec).slice("fortrip ".length)).join(" and ");
|
|
103
110
|
function overallHelp() {
|
|
104
111
|
const rows = [
|
|
@@ -118,32 +125,57 @@ ${commandList(rows)}
|
|
|
118
125
|
Options:
|
|
119
126
|
${optionList(GLOBAL_FLAGS)}
|
|
120
127
|
|
|
121
|
-
Output is
|
|
122
|
-
|
|
128
|
+
Output is compact text on stdout; --json prints it as one line of JSON.
|
|
129
|
+
Errors go to stderr, as text or, with --json, as JSON.
|
|
130
|
+
Exit codes: 0 ok, 1 error (retry once when the error says it is retryable),
|
|
123
131
|
2 invalid arguments, 3 not logged in, 4 rate limited.
|
|
124
132
|
|
|
125
133
|
Environment:
|
|
126
|
-
FORTRIP_TOKEN
|
|
127
|
-
FORTRIP_API_URL
|
|
128
|
-
|
|
134
|
+
FORTRIP_TOKEN Use this token instead of the saved login (CI, sandboxes)
|
|
135
|
+
FORTRIP_API_URL API base URL (default ${DEFAULT_API_URL})
|
|
136
|
+
FORTRIP_MCP_URL MCP server URL for fortrip mcp (default ${DEFAULT_MCP_URL})
|
|
137
|
+
FORTRIP_DISPLAY_CURRENCY Default --display-currency for ${CURRENCY_COMMANDS}
|
|
129
138
|
`;
|
|
130
139
|
}
|
|
131
140
|
|
|
132
141
|
// src/commands.ts
|
|
133
|
-
var
|
|
134
|
-
var
|
|
135
|
-
var
|
|
136
|
-
var
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
142
|
+
var UPPER_CASE = /* @__PURE__ */ new Set(["displayCurrency", "destination.countryCode"]);
|
|
143
|
+
var FROM_ENV = { displayCurrency: "FORTRIP_DISPLAY_CURRENCY" };
|
|
144
|
+
var ONE_ITEM_LISTS = /* @__PURE__ */ new Set(["items"]);
|
|
145
|
+
var GUEST_FIELD = "items.hotel.rooms.guests";
|
|
146
|
+
var ROOMS_FIELD = "items.hotel.rooms";
|
|
147
|
+
var locale = (input) => input.locale;
|
|
148
|
+
var ROUTES = {
|
|
149
|
+
search_hotels: { method: "POST", path: () => API.hotelSearch },
|
|
150
|
+
get_hotel: {
|
|
151
|
+
method: "GET",
|
|
152
|
+
path: (input) => API.hotel(input.hotelId, { rooms: input.include === "rooms", locale: locale(input) })
|
|
153
|
+
},
|
|
154
|
+
get_hotel_offers: { method: "POST", path: () => API.hotelOffers },
|
|
155
|
+
book_hotel: { method: "POST", path: () => API.orders },
|
|
156
|
+
get_order: {
|
|
157
|
+
method: "GET",
|
|
158
|
+
path: (input) => API.order(input.orderId, { locale: locale(input) })
|
|
159
|
+
},
|
|
160
|
+
list_orders: {
|
|
161
|
+
method: "GET",
|
|
162
|
+
path: (input) => {
|
|
163
|
+
const query = new URLSearchParams();
|
|
164
|
+
for (const name of ["cursor", "limit", "locale"]) {
|
|
165
|
+
if (input[name] !== void 0) query.set(name, String(input[name]));
|
|
166
|
+
}
|
|
167
|
+
const text = query.toString();
|
|
168
|
+
return text ? `${API.orders}?${text}` : API.orders;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
140
171
|
};
|
|
172
|
+
var PATH_FIELDS = /* @__PURE__ */ new Set(["hotelId", "orderId"]);
|
|
141
173
|
function parseOptions(spec, argv) {
|
|
142
174
|
const options = {};
|
|
143
175
|
for (const option of [...spec.flags, ...spec.options ?? []]) {
|
|
144
176
|
options[option.flag] = {
|
|
145
177
|
type: option.type === "boolean" ? "boolean" : "string",
|
|
146
|
-
...option.short ? { short: option.short } : {},
|
|
178
|
+
..."short" in option && option.short ? { short: option.short } : {},
|
|
147
179
|
..."multiple" in option && option.multiple ? { multiple: true } : {}
|
|
148
180
|
};
|
|
149
181
|
}
|
|
@@ -175,38 +207,83 @@ function parseOptions(spec, argv) {
|
|
|
175
207
|
return { values: parsed.values, positionals: parsed.positionals };
|
|
176
208
|
}
|
|
177
209
|
function parseCommand(spec, argv, env) {
|
|
178
|
-
const
|
|
179
|
-
if (!
|
|
180
|
-
throw new Error(`${commandInvocation(spec)} sends no request`);
|
|
181
|
-
}
|
|
210
|
+
const route = spec.tool ? ROUTES[spec.tool] : void 0;
|
|
211
|
+
if (!route) throw new Error(`${commandInvocation(spec)} sends no request`);
|
|
182
212
|
const { values, positionals: positionals2 } = parseOptions(spec, argv);
|
|
183
213
|
const input = baseBody(values.body);
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
214
|
+
const wanted = spec.positionals ?? [];
|
|
215
|
+
if (positionals2.length !== wanted.length) throw usageError(`Usage: ${usageLine(spec)}`);
|
|
216
|
+
wanted.forEach((positional, index) => {
|
|
217
|
+
if (positional.field) input[positional.field] = positionals2[index];
|
|
218
|
+
});
|
|
219
|
+
setFlags(spec, values, input);
|
|
220
|
+
const fromEnv = /* @__PURE__ */ new Map();
|
|
221
|
+
for (const [field, name] of Object.entries(FROM_ENV)) {
|
|
222
|
+
const fallback = env[name]?.trim().toUpperCase();
|
|
223
|
+
if (fallback && input[field] === void 0 && spec.flags.some((flag) => flag.field === field)) {
|
|
224
|
+
input[field] = fallback;
|
|
225
|
+
fromEnv.set(`/${field}`, name);
|
|
226
|
+
}
|
|
187
227
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
228
|
+
check(spec, input, route.method === "GET", fromEnv);
|
|
229
|
+
const path = route.path(input);
|
|
230
|
+
return route.method === "POST" ? { method: "POST", path, body: input } : { method: "GET", path };
|
|
231
|
+
}
|
|
232
|
+
function check(spec, input, isGet, fromEnv) {
|
|
233
|
+
const schema = TOOLS.find((tool) => tool.name === spec.tool)?.input;
|
|
234
|
+
const result = schema?.safeParse(input);
|
|
235
|
+
if (!result || result.success) return;
|
|
236
|
+
const pointer = (path) => {
|
|
237
|
+
const keys = path.map((key) => String(key).replaceAll("~", "~0").replaceAll("/", "~1"));
|
|
238
|
+
const where = isGet && keys[0] !== void 0 ? [PATH_FIELDS.has(keys[0]) ? "path" : "query"] : [];
|
|
239
|
+
return [...where, ...keys].map((key) => `/${key}`).join("");
|
|
240
|
+
};
|
|
241
|
+
const issues = result.error.issues.flatMap(
|
|
242
|
+
(issue) => issue.code === "unrecognized_keys" ? issue.keys.map((key) => ({
|
|
243
|
+
pointer: pointer([...issue.path, key]),
|
|
244
|
+
message: `Unknown field: ${key}`
|
|
245
|
+
})) : [{ pointer: pointer(issue.path), message: issue.message }]
|
|
246
|
+
).map(({ pointer: pointer2, message }) => {
|
|
247
|
+
const name = fromEnv.get(pointer2);
|
|
248
|
+
return { pointer: pointer2, message: name ? `${name}: ${message}` : message };
|
|
249
|
+
});
|
|
250
|
+
throw new CliError(
|
|
251
|
+
`Not sent: ${commandInvocation(spec)} was given an invalid request.`,
|
|
252
|
+
EXIT.usage,
|
|
253
|
+
{ details: { issues } }
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
function setFlags(spec, values, input) {
|
|
257
|
+
const given = spec.flags.filter((flag) => values[flag.flag] !== void 0);
|
|
258
|
+
const picked = /* @__PURE__ */ new Map();
|
|
259
|
+
for (const flag of given) {
|
|
260
|
+
for (const [field, value] of Object.entries(flag.sets ?? {})) {
|
|
261
|
+
const earlier = picked.get(field);
|
|
262
|
+
if (earlier && earlier.value !== value) {
|
|
263
|
+
throw usageError(
|
|
264
|
+
`--${earlier.flag} and --${flag.flag} can't be given together: they set ${field} to "${earlier.value}" and "${value}"`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
if (!earlier) picked.set(field, { value, flag: flag.flag });
|
|
193
268
|
}
|
|
194
|
-
return { path: API.hotel(parsed, input.includeRooms === true) };
|
|
195
269
|
}
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
|
|
270
|
+
for (const [field, { value }] of picked) {
|
|
271
|
+
const parent = field.split(".").slice(0, -1).join(".");
|
|
272
|
+
const key = field.split(".").at(-1);
|
|
273
|
+
const existing = parent ? getField(input, parent) : input;
|
|
274
|
+
if (isObject(existing) && existing[key] !== void 0 && existing[key] !== value) {
|
|
275
|
+
setField(input, parent, {});
|
|
276
|
+
}
|
|
277
|
+
setField(input, field, value);
|
|
199
278
|
}
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
);
|
|
279
|
+
for (const flag of given) {
|
|
280
|
+
const raw = values[flag.flag];
|
|
281
|
+
if (flag.field === GUEST_FIELD) {
|
|
282
|
+
setField(input, ROOMS_FIELD, withGuests(getField(input, ROOMS_FIELD), parseGuests(raw)));
|
|
283
|
+
} else {
|
|
284
|
+
setField(input, flag.field, readFlag(flag, raw));
|
|
285
|
+
}
|
|
208
286
|
}
|
|
209
|
-
return { body: input, path: post };
|
|
210
287
|
}
|
|
211
288
|
function baseBody(raw) {
|
|
212
289
|
if (typeof raw !== "string") return {};
|
|
@@ -220,76 +297,125 @@ function baseBody(raw) {
|
|
|
220
297
|
}
|
|
221
298
|
try {
|
|
222
299
|
const parsed = JSON.parse(text);
|
|
223
|
-
if (
|
|
224
|
-
return parsed;
|
|
225
|
-
}
|
|
300
|
+
if (isObject(parsed)) return parsed;
|
|
226
301
|
} catch {
|
|
227
302
|
}
|
|
228
303
|
throw usageError("--body must be a JSON object");
|
|
229
304
|
}
|
|
230
|
-
function
|
|
305
|
+
function isObject(value) {
|
|
306
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
307
|
+
}
|
|
308
|
+
function step(node, part, create) {
|
|
309
|
+
if (ONE_ITEM_LISTS.has(part)) {
|
|
310
|
+
let list = node[part];
|
|
311
|
+
if (!Array.isArray(list)) {
|
|
312
|
+
if (!create) return void 0;
|
|
313
|
+
list = node[part] = [];
|
|
314
|
+
}
|
|
315
|
+
const items = list;
|
|
316
|
+
if (!isObject(items[0])) {
|
|
317
|
+
if (!create) return void 0;
|
|
318
|
+
items[0] = {};
|
|
319
|
+
}
|
|
320
|
+
return items[0];
|
|
321
|
+
}
|
|
322
|
+
if (!isObject(node[part])) {
|
|
323
|
+
if (!create) return void 0;
|
|
324
|
+
node[part] = {};
|
|
325
|
+
}
|
|
326
|
+
return node[part];
|
|
327
|
+
}
|
|
328
|
+
function getField(body, field) {
|
|
329
|
+
let node = body;
|
|
231
330
|
const path = field.split(".");
|
|
232
331
|
const last = path.pop();
|
|
233
|
-
let node = body;
|
|
234
332
|
for (const part of path) {
|
|
235
|
-
|
|
236
|
-
node[part] = next && typeof next === "object" && !Array.isArray(next) ? next : {};
|
|
237
|
-
node = node[part];
|
|
333
|
+
node = node && step(node, part, false);
|
|
238
334
|
}
|
|
335
|
+
return node?.[last];
|
|
336
|
+
}
|
|
337
|
+
function setField(body, field, value) {
|
|
338
|
+
const path = field.split(".");
|
|
339
|
+
const last = path.pop();
|
|
340
|
+
let node = body;
|
|
341
|
+
for (const part of path) node = step(node, part, true);
|
|
239
342
|
node[last] = value;
|
|
240
343
|
}
|
|
241
344
|
function readFlag(flag, raw) {
|
|
242
|
-
if (flag.type === "boolean") return
|
|
345
|
+
if (flag.type === "boolean") return flag.value ?? true;
|
|
243
346
|
const given = Array.isArray(raw) ? raw : [raw];
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
347
|
+
switch (flag.field) {
|
|
348
|
+
case "destination.location":
|
|
349
|
+
return parsePoint(given[0]);
|
|
350
|
+
case "stay.rooms":
|
|
351
|
+
return given.map(parseRoom);
|
|
352
|
+
case "filters.price.maxPerRoomNight":
|
|
353
|
+
return { amount: given[0].trim(), currency: "USD" };
|
|
354
|
+
}
|
|
355
|
+
const parsed = given.map((value) => {
|
|
356
|
+
if (flag.type === "number") return num(flag.flag, value);
|
|
357
|
+
return UPPER_CASE.has(flag.field) ? value.trim().toUpperCase() : value;
|
|
254
358
|
});
|
|
255
|
-
return flag.multiple ?
|
|
359
|
+
return flag.multiple ? parsed : parsed[0];
|
|
256
360
|
}
|
|
257
361
|
function num(flag, value) {
|
|
258
|
-
const
|
|
259
|
-
if (
|
|
362
|
+
const text = value.trim();
|
|
363
|
+
if (!/^[+-]?\d+(\.\d+)?$/.test(text)) {
|
|
260
364
|
throw usageError(`--${flag} must be a number, got "${value}"`);
|
|
261
365
|
}
|
|
262
|
-
return
|
|
263
|
-
}
|
|
264
|
-
function id(flag, value) {
|
|
265
|
-
const parsed = Number(value.trim());
|
|
266
|
-
if (!/^\d+$/.test(value.trim()) || !Number.isSafeInteger(parsed)) {
|
|
267
|
-
throw usageError(`--${flag} must be a numeric id, got "${value}"`);
|
|
268
|
-
}
|
|
269
|
-
return parsed;
|
|
366
|
+
return Number(text);
|
|
270
367
|
}
|
|
271
|
-
function
|
|
368
|
+
function parsePoint(value) {
|
|
272
369
|
const parts = value.split(",").map((part) => part.trim());
|
|
273
|
-
if (parts.length !==
|
|
274
|
-
throw usageError(
|
|
370
|
+
if (parts.length !== 2 || parts.some((part) => !/^[+-]?\d+(\.\d+)?$/.test(part))) {
|
|
371
|
+
throw usageError(`--point must be lat,lng (e.g. 13.75,100.5), got "${value}"`);
|
|
275
372
|
}
|
|
276
|
-
const [lat, lng
|
|
277
|
-
return { lat, lng
|
|
373
|
+
const [lat, lng] = parts.map(Number);
|
|
374
|
+
return { lat, lng };
|
|
278
375
|
}
|
|
279
|
-
function
|
|
280
|
-
const match = value.
|
|
281
|
-
|
|
282
|
-
if (words.length < 2) {
|
|
376
|
+
function parseRoom(value) {
|
|
377
|
+
const match = value.replace(/\s+/g, "").match(/^(\d+)(?::(\d+(?:,\d+)*))?$/);
|
|
378
|
+
if (!match) {
|
|
283
379
|
throw usageError(
|
|
284
|
-
`--
|
|
380
|
+
`--room must be adults or adults:age,age with each child's age (e.g. 2 or 2:6,9), got "${value}"`
|
|
285
381
|
);
|
|
286
382
|
}
|
|
287
383
|
return {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
roomNo: match?.[2] ? Number(match[2]) : 1
|
|
384
|
+
adults: Number(match[1]),
|
|
385
|
+
childrenAges: match[2] ? match[2].split(",").map(Number) : []
|
|
291
386
|
};
|
|
292
387
|
}
|
|
388
|
+
function parseGuests(values) {
|
|
389
|
+
const rooms = /* @__PURE__ */ new Map();
|
|
390
|
+
for (const value of values) {
|
|
391
|
+
const parts = value.split(":").map((part) => part.trim());
|
|
392
|
+
const [room, firstName, lastName] = parts;
|
|
393
|
+
const number = Number(room);
|
|
394
|
+
if (parts.length !== 3 || !/^\d+$/.test(room ?? "")) {
|
|
395
|
+
throw usageError(`--guest must be room:first:last (e.g. 1:John:Smith), got "${value}"`);
|
|
396
|
+
}
|
|
397
|
+
if (number < 1 || number > LIMITS.rooms) {
|
|
398
|
+
throw usageError(`--guest room must be 1 to ${LIMITS.rooms}, got "${value}"`);
|
|
399
|
+
}
|
|
400
|
+
rooms.set(number, [...rooms.get(number) ?? [], { firstName, lastName }]);
|
|
401
|
+
}
|
|
402
|
+
return rooms;
|
|
403
|
+
}
|
|
404
|
+
function withGuests(fromBody, guests) {
|
|
405
|
+
const rooms = Array.isArray(fromBody) ? [...fromBody] : [];
|
|
406
|
+
for (const [number, list] of guests) {
|
|
407
|
+
const room = rooms[number - 1];
|
|
408
|
+
rooms[number - 1] = { ...isObject(room) ? room : {}, guests: list };
|
|
409
|
+
}
|
|
410
|
+
for (let index = 0; index < rooms.length; index++) {
|
|
411
|
+
if (rooms[index] === void 0) {
|
|
412
|
+
throw usageError(
|
|
413
|
+
`--guest rooms are numbered from 1 with none skipped: no guest for room ${index + 1}`
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return rooms;
|
|
418
|
+
}
|
|
293
419
|
|
|
294
420
|
// src/guide.ts
|
|
295
421
|
function readGuide(topic) {
|
|
@@ -303,8 +429,11 @@ function globalFlag(name) {
|
|
|
303
429
|
}
|
|
304
430
|
var HELP_FLAGS = globalFlag("help");
|
|
305
431
|
var VERSION_FLAGS = globalFlag("version");
|
|
432
|
+
var JSON_FLAG = "--json";
|
|
306
433
|
var RENAMED = /* @__PURE__ */ new Map([
|
|
307
|
-
["hotel", { group: "hotels", name: "get" }]
|
|
434
|
+
["hotel", { group: "hotels", name: "get" }],
|
|
435
|
+
["rates", { group: "hotels", name: "offers" }],
|
|
436
|
+
["hotels rates", { group: "hotels", name: "offers" }]
|
|
308
437
|
]);
|
|
309
438
|
function findCommand(argv) {
|
|
310
439
|
const [word, ...afterWord] = argv;
|
|
@@ -318,8 +447,9 @@ function findCommand(argv) {
|
|
|
318
447
|
(candidate) => candidate.group === group.name && candidate.name === name
|
|
319
448
|
);
|
|
320
449
|
if (!spec2) {
|
|
450
|
+
const renamed2 = RENAMED.get(`${group.name} ${name}`);
|
|
321
451
|
throw usageError(
|
|
322
|
-
`Unknown command "${group.name} ${name}". Run \`fortrip ${group.name} --help\`.`
|
|
452
|
+
renamed2 ? `Unknown command "${group.name} ${name}". Did you mean \`${commandInvocation(renamed2)}\`?` : `Unknown command "${group.name} ${name}". Run \`fortrip ${group.name} --help\`.`
|
|
323
453
|
);
|
|
324
454
|
}
|
|
325
455
|
return { spec: spec2, rest };
|
|
@@ -377,26 +507,39 @@ async function run(argv) {
|
|
|
377
507
|
if (process.env.FORTRIP_TOKEN) {
|
|
378
508
|
log("Note: FORTRIP_TOKEN is set and takes precedence over this login.");
|
|
379
509
|
}
|
|
380
|
-
return login({ apiUrl, openBrowser: !values["no-browser"], log });
|
|
510
|
+
return account(await login({ apiUrl, openBrowser: !values["no-browser"], log }));
|
|
381
511
|
}
|
|
382
|
-
case "logout":
|
|
512
|
+
case "logout": {
|
|
383
513
|
parseOptions(spec, rest);
|
|
384
|
-
|
|
514
|
+
const data2 = await logout(apiUrl);
|
|
515
|
+
return { data: data2, text: `Logged out of ${apiUrl}.` };
|
|
516
|
+
}
|
|
385
517
|
case "whoami":
|
|
386
518
|
parseOptions(spec, rest);
|
|
387
|
-
return whoami(apiUrl);
|
|
519
|
+
return account(await whoami(apiUrl));
|
|
388
520
|
case "mcp": {
|
|
389
521
|
parseOptions(spec, rest);
|
|
390
|
-
const
|
|
391
|
-
|
|
522
|
+
const mcpUrl = resolveMcpUrl(apiUrl);
|
|
523
|
+
const { runMcpBridge } = await import("./mcp-bridge-3WLWV2KY.js");
|
|
524
|
+
await runMcpBridge(mcpUrl, apiUrl);
|
|
525
|
+
return void 0;
|
|
392
526
|
}
|
|
393
527
|
}
|
|
394
528
|
}
|
|
395
|
-
const {
|
|
396
|
-
|
|
529
|
+
const { method, path, body } = parseCommand(spec, rest, process.env);
|
|
530
|
+
const data = unwrap(await authorizedRequest(apiUrl, path, { method, body }));
|
|
531
|
+
return { data, text: renderToolResult(spec.tool, data) };
|
|
397
532
|
}
|
|
398
|
-
|
|
399
|
-
|
|
533
|
+
function account(data) {
|
|
534
|
+
const { apiUrl, userId, email, name } = data;
|
|
535
|
+
const given = (value) => value ?? "not given";
|
|
536
|
+
return {
|
|
537
|
+
data,
|
|
538
|
+
text: [
|
|
539
|
+
`Signed in to ${apiUrl}.`,
|
|
540
|
+
...fieldLines({ userId, email: given(email), name: given(name) })
|
|
541
|
+
].join("\n")
|
|
542
|
+
};
|
|
400
543
|
}
|
|
401
544
|
function wantsHelp(args) {
|
|
402
545
|
for (const arg of args) {
|
|
@@ -405,21 +548,33 @@ function wantsHelp(args) {
|
|
|
405
548
|
}
|
|
406
549
|
return false;
|
|
407
550
|
}
|
|
551
|
+
function takeJson(argv) {
|
|
552
|
+
const end = argv.indexOf("--");
|
|
553
|
+
const before = end === -1 ? argv : argv.slice(0, end);
|
|
554
|
+
const after = end === -1 ? [] : argv.slice(end);
|
|
555
|
+
const kept = before.filter((arg) => arg !== JSON_FLAG);
|
|
556
|
+
return { json: kept.length < before.length, argv: [...kept, ...after] };
|
|
557
|
+
}
|
|
408
558
|
async function main() {
|
|
559
|
+
const { json, argv } = takeJson(process.argv.slice(2));
|
|
409
560
|
try {
|
|
410
|
-
const
|
|
411
|
-
if (
|
|
412
|
-
const
|
|
413
|
-
|
|
561
|
+
const answer = await run(argv);
|
|
562
|
+
if (answer !== void 0) {
|
|
563
|
+
const { data, text } = answer;
|
|
564
|
+
if (json) {
|
|
565
|
+
const output = data && typeof data === "object" && !Array.isArray(data) ? { ...data, ...notices } : data;
|
|
566
|
+
process.stdout.write(`${JSON.stringify(output)}
|
|
567
|
+
`);
|
|
568
|
+
} else {
|
|
569
|
+
process.stdout.write(`${[text, ...fieldLines(notices)].join("\n")}
|
|
414
570
|
`);
|
|
571
|
+
}
|
|
415
572
|
}
|
|
416
573
|
return EXIT.ok;
|
|
417
574
|
} catch (error) {
|
|
418
575
|
const failure = error instanceof CliError ? error : new CliError(error?.message ?? String(error), EXIT.error);
|
|
419
|
-
process.stderr.write(
|
|
420
|
-
|
|
421
|
-
`
|
|
422
|
-
);
|
|
576
|
+
process.stderr.write(`${formatError(failure, json)}
|
|
577
|
+
`);
|
|
423
578
|
return failure.exitCode;
|
|
424
579
|
}
|
|
425
580
|
}
|