@voyagier/cli
Advanced tools
+23
-1
@@ -168,2 +168,23 @@ # Voyagier CLI — Agent Reference | ||
| ### Loyalty programs (optional, best-effort at checkout) | ||
| Travellers can carry loyalty programs; checkout applies them automatically. | ||
| **A booking never fails or blocks because of loyalty** — if a program can't be | ||
| applied it is silently skipped, so no error ≠ guaranteed credit. | ||
| - **Frequent flyer:** `--frequent-flyer AIRLINE:NUMBER` (repeatable, e.g. | ||
| `--frequent-flyer DL:1234567`). The member number is sent to the airline **verbatim** | ||
| — pass it exactly as issued. Applied per matching passenger on flight checkout. | ||
| - **Hotel:** `--hotel-loyalty CHAIN:NUMBER` (repeatable, e.g. | ||
| `--hotel-loyalty HI:12345678`). The member number is **digits only — do NOT | ||
| include the chain code prefix** (checkout builds the id as chain + number; a | ||
| prefixed number would double the chain and never apply). Applied for the | ||
| **primary guest only**, and only when the program's chain matches the booked | ||
| property's chain. | ||
| - On `travellers update`: `--frequent-flyer`/`--hotel-loyalty` **replace** the full | ||
| list; `--clear-frequent-flyer`/`--clear-hotel-loyalty` remove all programs. Omitting | ||
| the flags leaves programs untouched. | ||
| - Numbers are encrypted at rest server-side; reads only ever return the | ||
| code + `last4` — there is no way to read a stored number back. | ||
| --- | ||
@@ -404,4 +425,5 @@ | ||
| voyagier travellers add --plan <id> --first <f> --last <l> --type Adult|Child|Infant --json | ||
| voyagier travellers add --plan <id> --first <f> --last <l> --frequent-flyer DL:1234567 --hotel-loyalty HI:12345678 --json | ||
| voyagier travellers list --plan <id> --json | ||
| voyagier travellers update <travellerId> [...] --json | ||
| voyagier travellers update <travellerId> [...] --json # incl. --frequent-flyer / --hotel-loyalty (replace) and --clear-frequent-flyer / --clear-hotel-loyalty | ||
| voyagier travellers remove <travellerId> --json | ||
@@ -408,0 +430,0 @@ ``` |
@@ -14,2 +14,65 @@ import { printPlanFooter } from "../plan-footer.js"; | ||
| } | ||
| function collect(value, previous) { | ||
| return [...previous, value]; | ||
| } | ||
| function maskLoyaltyValue(value) { | ||
| return value.length > 4 ? `••••${value.slice(-4)}` : "••••"; | ||
| } | ||
| function requirePassportNumberWithMetadata(opts) { | ||
| if (opts.passportNumber) | ||
| return; | ||
| const provided = [ | ||
| opts.passportCountry && "--passport-country", | ||
| opts.passportNationality && "--passport-nationality", | ||
| opts.passportExpiry && "--passport-expiry", | ||
| ].filter(Boolean); | ||
| if (provided.length > 0) { | ||
| throw new CliError(CliErrorCode.VALIDATION, `${provided.join(", ")} require${provided.length === 1 ? "s" : ""} --passport-number — passport metadata is ignored without it`); | ||
| } | ||
| } | ||
| function parseLoyalty(raw, kind) { | ||
| const label = kind === "air" ? "--frequent-flyer" : "--hotel-loyalty"; | ||
| const example = kind === "air" ? "DL:1234567" : "HI:12345678"; | ||
| const sep = raw.indexOf(":"); | ||
| if (sep === -1) { | ||
| throw new CliError(CliErrorCode.VALIDATION, `${label} expects CODE:NUMBER (e.g. ${example}), got: "${maskLoyaltyValue(raw)}"`); | ||
| } | ||
| const code = raw.slice(0, sep).trim().toUpperCase(); | ||
| const membershipNumber = raw.slice(sep + 1).trim(); | ||
| const codePattern = kind === "air" ? /^[A-Z0-9]{2}$/ : /^[A-Z]{2}$/; | ||
| if (!codePattern.test(code)) { | ||
| const kindWord = kind === "air" ? "airline code" : "chain code"; | ||
| throw new CliError(CliErrorCode.VALIDATION, `${label}: ${kindWord} must be exactly 2 ${kind === "air" ? "characters" : "letters"} (e.g. ${example.split(":")[0]}), got: "${code}"`); | ||
| } | ||
| if (membershipNumber.length === 0) { | ||
| throw new CliError(CliErrorCode.VALIDATION, `${label}: missing member number after "${code}:"`); | ||
| } | ||
| if (kind === "hotel" && !/^\d+$/.test(membershipNumber)) { | ||
| const hint = membershipNumber.toUpperCase().startsWith(code) | ||
| ? ` — do not include the chain code, checkout prefixes "${code}" automatically` | ||
| : ""; | ||
| throw new CliError(CliErrorCode.VALIDATION, `${label}: member number must be digits only${hint}, got: "${maskLoyaltyValue(membershipNumber)}"`); | ||
| } | ||
| return { code, membershipNumber }; | ||
| } | ||
| function toAirLoyaltyInput(values) { | ||
| return values.map((v) => { | ||
| const p = parseLoyalty(v, "air"); | ||
| return { airlineCode: p.code, membershipNumber: p.membershipNumber }; | ||
| }); | ||
| } | ||
| function toHotelLoyaltyInput(values) { | ||
| return values.map((v) => { | ||
| const p = parseLoyalty(v, "hotel"); | ||
| return { chainCode: p.code, membershipNumber: p.membershipNumber }; | ||
| }); | ||
| } | ||
| function loyaltySummary(t) { | ||
| const bits = []; | ||
| for (const p of t.frequentFlyerPrograms ?? []) | ||
| bits.push(`✈ ${p.airlineCode}${p.last4 ? ` ••••${p.last4}` : ""}`); | ||
| for (const p of t.hotelLoyaltyPrograms ?? []) | ||
| bits.push(`🏨 ${p.chainCode}${p.last4 ? ` ••••${p.last4}` : ""}`); | ||
| return bits.length > 0 ? bits.join(" · ") : undefined; | ||
| } | ||
| export function registerTravellerCommands(program) { | ||
@@ -32,2 +95,4 @@ const travellers = program.command("travellers").description("Manage trip plan travellers"); | ||
| .option("--passport-expiry <date>", "Passport expiration (YYYY-MM)") | ||
| .option("--frequent-flyer <program>", "Frequent-flyer program AIRLINE:NUMBER (repeatable, e.g. DL:1234567)", collect, []) | ||
| .option("--hotel-loyalty <program>", "Hotel loyalty program CHAIN:NUMBER — member number digits only, no chain prefix (repeatable, e.g. HI:12345678)", collect, []) | ||
| .option("--self", "Auto-fill from your saved profile (voyagier auth setup)") | ||
@@ -132,2 +197,3 @@ .option("--json", "Output raw JSON") | ||
| } | ||
| requirePassportNumberWithMetadata(opts); | ||
| if (opts.passportNumber) { | ||
@@ -143,2 +209,6 @@ const passportInput = { | ||
| } | ||
| if (opts.frequentFlyer.length > 0) | ||
| input.frequentFlyerPrograms = toAirLoyaltyInput(opts.frequentFlyer); | ||
| if (opts.hotelLoyalty.length > 0) | ||
| input.hotelLoyaltyPrograms = toHotelLoyaltyInput(opts.hotelLoyalty); | ||
| const data = await graphql(CREATE_TRAVELLER, { tripPlanId: opts.plan, input }); | ||
@@ -161,2 +231,5 @@ const t = data.createTripPlanTraveller; | ||
| console.log(chalk.dim(` Gender: ${t.gender}`)); | ||
| const loyalty = loyaltySummary(t); | ||
| if (loyalty) | ||
| console.log(chalk.dim(` Loyalty: ${loyalty}`)); | ||
| await printPlanFooter(opts.plan); | ||
@@ -223,2 +296,5 @@ } | ||
| console.log(chalk.dim(` ${details.join(" · ")}`)); | ||
| const loyalty = loyaltySummary(t); | ||
| if (loyalty) | ||
| console.log(chalk.dim(` Loyalty: ${loyalty}`)); | ||
| const missing = []; | ||
@@ -276,2 +352,6 @@ if (!t.dateOfBirth) | ||
| .option("--passport-expiry <date>", "Passport expiration (YYYY-MM)") | ||
| .option("--frequent-flyer <program>", "Replace frequent-flyer programs with AIRLINE:NUMBER (repeatable, e.g. DL:1234567)", collect, []) | ||
| .option("--hotel-loyalty <program>", "Replace hotel loyalty programs with CHAIN:NUMBER — member number digits only, no chain prefix (repeatable, e.g. HI:12345678)", collect, []) | ||
| .option("--clear-frequent-flyer", "Remove all frequent-flyer programs") | ||
| .option("--clear-hotel-loyalty", "Remove all hotel loyalty programs") | ||
| .option("--json", "Output raw JSON") | ||
@@ -301,2 +381,3 @@ .action(async (id, opts) => { | ||
| } | ||
| requirePassportNumberWithMetadata(opts); | ||
| if (opts.passportNumber) { | ||
@@ -312,4 +393,18 @@ const passportInput = { | ||
| } | ||
| if (opts.frequentFlyer.length > 0 && opts.clearFrequentFlyer) { | ||
| throw new CliError(CliErrorCode.VALIDATION, "--frequent-flyer and --clear-frequent-flyer are mutually exclusive"); | ||
| } | ||
| if (opts.hotelLoyalty.length > 0 && opts.clearHotelLoyalty) { | ||
| throw new CliError(CliErrorCode.VALIDATION, "--hotel-loyalty and --clear-hotel-loyalty are mutually exclusive"); | ||
| } | ||
| if (opts.clearFrequentFlyer) | ||
| input.frequentFlyerPrograms = []; | ||
| else if (opts.frequentFlyer.length > 0) | ||
| input.frequentFlyerPrograms = toAirLoyaltyInput(opts.frequentFlyer); | ||
| if (opts.clearHotelLoyalty) | ||
| input.hotelLoyaltyPrograms = []; | ||
| else if (opts.hotelLoyalty.length > 0) | ||
| input.hotelLoyaltyPrograms = toHotelLoyaltyInput(opts.hotelLoyalty); | ||
| if (Object.keys(input).length === 0) { | ||
| fatal("Nothing to update. Provide at least one of: --first, --last, --email, --dob, --gender, --type, --phone, --passport-number"); | ||
| fatal("Nothing to update. Provide at least one of: --first, --last, --email, --dob, --gender, --type, --phone, --passport-number, --passport-country, --passport-nationality, --passport-expiry, --frequent-flyer, --hotel-loyalty, --clear-frequent-flyer, --clear-hotel-loyalty"); | ||
| } | ||
@@ -327,2 +422,5 @@ const data = await graphql(UPDATE_TRAVELLER, { id, input }); | ||
| console.log(chalk.dim(` Email: ${t.email}`)); | ||
| const loyalty = loyaltySummary(t); | ||
| if (loyalty) | ||
| console.log(chalk.dim(` Loyalty: ${loyalty}`)); | ||
| } | ||
@@ -329,0 +427,0 @@ catch (err) { |
+10
-2
@@ -46,3 +46,9 @@ import { z } from "zod"; | ||
| export function buildAddTravellerArgs(i) { | ||
| return ["travellers", "add", "--plan", i.plan_id, "--first", i.first, "--last", i.last, "--type", i.type ?? "Adult", "--json"]; | ||
| const args = ["travellers", "add", "--plan", i.plan_id, "--first", i.first, "--last", i.last, "--type", i.type ?? "Adult"]; | ||
| for (const p of i.frequent_flyer ?? []) | ||
| args.push("--frequent-flyer", p); | ||
| for (const p of i.hotel_loyalty ?? []) | ||
| args.push("--hotel-loyalty", p); | ||
| args.push("--json"); | ||
| return args; | ||
| } | ||
@@ -157,3 +163,3 @@ export function buildSearchFlightsArgs(i) { | ||
| name: "add_traveller", | ||
| description: "Add a traveller to a trip plan. Travellers are required before search. Gender and date of birth are required at flight checkout; passport data hard-gates international reservations (set those via the CLI travellers update later).", | ||
| description: "Add a traveller to a trip plan. Travellers are required before search. Gender and date of birth are required at flight checkout; passport data hard-gates international reservations (set those via the CLI travellers update later). Loyalty programs are applied at checkout best-effort — a booking never fails because of them.", | ||
| timeoutMs: T.short, | ||
@@ -165,2 +171,4 @@ inputSchema: { | ||
| type: z.string().optional().describe("Traveller type: Adult | Child | Infant. Default Adult."), | ||
| frequent_flyer: z.array(z.string()).optional().describe('Frequent-flyer programs as "AIRLINE:NUMBER", e.g. ["DL:1234567"]. Member number exactly as the airline issued it.'), | ||
| hotel_loyalty: z.array(z.string()).optional().describe('Hotel loyalty programs as "CHAIN:NUMBER", e.g. ["HI:12345678"]. Member number is digits only — do NOT include the chain code prefix.'), | ||
| }, | ||
@@ -167,0 +175,0 @@ buildArgs: (i) => buildAddTravellerArgs(i), |
+6
-0
@@ -326,2 +326,4 @@ export const GET_CART = ` | ||
| id firstName lastName email dateOfBirth gender declaredTravellerType | ||
| frequentFlyerPrograms { airlineCode last4 } | ||
| hotelLoyaltyPrograms { chainCode last4 } | ||
| } | ||
@@ -342,2 +344,4 @@ } | ||
| passport { last4 issueCountry } | ||
| frequentFlyerPrograms { airlineCode last4 } | ||
| hotelLoyaltyPrograms { chainCode last4 } | ||
| } | ||
@@ -360,2 +364,4 @@ } | ||
| id firstName lastName email dateOfBirth gender declaredTravellerType | ||
| frequentFlyerPrograms { airlineCode last4 } | ||
| hotelLoyaltyPrograms { chainCode last4 } | ||
| } | ||
@@ -362,0 +368,0 @@ } |
+2
-1
| { | ||
| "name": "@voyagier/cli", | ||
| "version": "2.12.0", | ||
| "version": "2.13.0", | ||
| "mcpName": "com.voyagier/cli", | ||
| "description": "Agent-ready travel CLI — search, plan, quote, and book real trips (flights, hotels, activities) against the Voyagier platform. Built for AI agents: --json everywhere, uniform error codes, price-gated checkout, printable agent reference (voyagier agent-docs).", | ||
@@ -5,0 +6,0 @@ "type": "module", |
+2
-0
@@ -70,2 +70,4 @@ --- | ||
| voyagier travellers add --plan <PLAN_ID> --first John --last Smith --type Adult --json | ||
| # Optional loyalty (applied at checkout best-effort — never blocks a booking): | ||
| # --frequent-flyer DL:1234567 (FF number verbatim) · --hotel-loyalty HI:12345678 (digits only, NO chain prefix) | ||
@@ -72,0 +74,0 @@ # 4. Search → select. search --json returns a COMPACT envelope: |
Unidentified License
LicenseSomething that seems like a license was found, but its contents could not be matched with a known license.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Unidentified License
LicenseSomething that seems like a license was found, but its contents could not be matched with a known license.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
1178528
0.71%18430
0.62%