🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@voyagier/cli

Package Overview
Dependencies
Maintainers
2
Versions
40
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@voyagier/cli - npm Package Compare versions

Comparing version
2.16.0
to
2.17.0
+16
dist/plan-urls.js
import { getApiUrl } from "./config.js";
import { deriveBaseUrl } from "./utils.js";
function base(baseUrl) {
return baseUrl ?? deriveBaseUrl(getApiUrl());
}
export function clientPlanUrl(id, baseUrl) {
return `${base(baseUrl)}/me/trips/plans/${id}`;
}
export function advisorPlanUrl(id, baseUrl) {
return `${base(baseUrl)}/advisor/plans/${id}`;
}
export function planUrls(id, baseUrl) {
const b = base(baseUrl);
const clientUrl = clientPlanUrl(id, b);
return { url: clientUrl, clientUrl, advisorUrl: advisorPlanUrl(id, b) };
}
import { graphql } from "./api.js";
import { GET_SELECTION_WITH_MONITOR, GET_BLUEPRINT_MONITOR, REFRESH_SELECTION_OPTIONS, } from "./queries.js";
import { classifySelection, isTerminal, } from "./selection-status.js";
import { CliError, CliErrorCode } from "./errors.js";
export const DEFAULT_RETRY_AFTER_MS = 2000;
const MAX_DELAY_MS = 8000;
const BACKOFF_FACTOR = 1.5;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export async function loadSelectionState(selectionId, retryAfterMs, gql = graphql) {
const data = await gql(GET_SELECTION_WITH_MONITOR, { tripPlanSelectionId: selectionId });
const raw = data.getTripPlanSelection;
if (!raw || !raw.id) {
throw new CliError(CliErrorCode.NOT_FOUND, `Selection ${selectionId} not found.`);
}
let monitor = null;
if (raw.blueprintMonitorId) {
try {
const m = await gql(GET_BLUEPRINT_MONITOR, {
id: raw.blueprintMonitorId,
});
monitor = m.blueprintMonitor;
}
catch {
monitor = null;
}
}
const state = {
id: raw.id,
type: raw.type,
blueprintMonitorId: raw.blueprintMonitorId,
optionCount: (raw.options ?? []).length,
monitor: monitor
? {
id: monitor.id,
fetchedAt: monitor.fetchedAt,
lastFetchAttempt: monitor.lastFetchAttempt,
lastFetchError: monitor.lastFetchError,
}
: null,
};
return { raw, result: classifySelection(state, { retryAfterMs }) };
}
export async function pollSelectionOptions(selectionId, initial, opts, deps = {}) {
const gql = deps.gql ?? graphql;
const now = deps.now ?? Date.now;
const sleepFn = deps.sleepFn ?? sleep;
const retryAfterMs = opts.retryAfterMs ?? DEFAULT_RETRY_AFTER_MS;
let snap = initial;
try {
await gql(REFRESH_SELECTION_OPTIONS, { selectionId });
}
catch {
}
const startedAt = now();
const deadline = startedAt + opts.timeoutMs;
let delay = retryAfterMs;
let attempt = 0;
while (!isTerminal(snap.result.status) && now() < deadline) {
const remaining = deadline - now();
await sleepFn(Math.min(delay, Math.max(0, remaining)));
snap = await loadSelectionState(selectionId, retryAfterMs, gql);
attempt++;
deps.heartbeat?.({
attempt,
status: snap.result.status,
optionCount: snap.result.optionCount,
elapsedMs: now() - startedAt,
});
delay = Math.min(delay * BACKOFF_FACTOR, MAX_DELAY_MS);
}
if (!isTerminal(snap.result.status)) {
return { ...snap, result: { ...snap.result, status: "FETCHING", retryAfterMs } };
}
return snap;
}
export async function waitForSelectionOptions(selectionId, opts, deps = {}) {
const gql = deps.gql ?? graphql;
const retryAfterMs = opts.retryAfterMs ?? DEFAULT_RETRY_AFTER_MS;
const initial = await loadSelectionState(selectionId, retryAfterMs, gql);
if (isTerminal(initial.result.status))
return initial;
return pollSelectionOptions(selectionId, initial, opts, deps);
}
+19
-5

@@ -89,3 +89,5 @@ # Voyagier CLI — Agent Reference

# rows) — picking the identical id on outbound and return is the intended
# pattern, not a bug:
# pairing, not a bug. Operating airlines MAY differ between the two legs
# (mixed-carrier round trips are normal), so don't reject a pairing just
# because the return leg's carrier isn't the outbound's:
voyagier selection-options <RETURN_SELECTION_ID> --wait --json

@@ -234,3 +236,5 @@ voyagier select --selection-id <RETURN_SELECTION_ID> --option-id <OPTION_ID> --json

"title": "...",
"url": "https://app.voyagier.com/plans/..."
"url": "https://app.voyagier.com/me/trips/plans/...",
"clientUrl": "https://app.voyagier.com/me/trips/plans/...",
"advisorUrl": "https://app.voyagier.com/advisor/plans/..."
}

@@ -240,2 +244,4 @@ }

**Plan URL fields.** Payloads that link to a plan emit three fields: `clientUrl` (`<base>/me/trips/plans/{id}`) is the traveller-facing view a client opens; `advisorUrl` (`<base>/advisor/plans/{id}`) is the advisor-facing workspace. `url` is a back-compat alias of `clientUrl` — hand a client the `clientUrl`, and open the `advisorUrl` yourself. The older `<base>/plans/{id}` route is retired.
**Style B — flat / domain-specific** (clients, `plans list`/`create`, travellers, search, select, whoami — the older surfaces). `select` payloads are flat but DO carry `ok: true`, so `.ok` is checkable on every select outcome:

@@ -247,5 +253,5 @@

// clients upsert: { "client": { ... }, "ok": true, "created": false }
// plans create: { "id": "...", "title": "...", "url": "...", "planSummary": "..." }
// plans list: { "items": [...], "total": 12, "page": 1, "limit": 20 }
// search flights: { "tripPlanId": "...", "selectionId": "...", "optionCount": N, "topOptions": [≤10 summaries], "url": "..." } (--full swaps topOptions for the complete options[] dump)
// plans create: { "id": "...", "title": "...", "url": "...", "clientUrl": "...", "advisorUrl": "...", "planSummary": "..." }
// plans list: { "items": [...], "total": 12, "page": 1, "limit": 20 } (each item carries url/clientUrl/advisorUrl)
// search flights: { "tripPlanId": "...", "selectionId": "...", "optionCount": N, "topOptions": [≤10 summaries], "url": "...", "clientUrl": "...", "advisorUrl": "..." } (--full swaps topOptions for the complete options[] dump)
// select: { "ok": true, "success": true, "type": "option_selected", ... }

@@ -483,2 +489,8 @@ // selection-options: { "selectionId": "...", "status": "...", "optionCount": N, "options": [...] }

**Prices are party totals.** Every search option's price is the total for the searched traveller group, NOT per-person — do not multiply by traveller count. (Hotel search prices are additionally whole-STAY totals, not nightly.) `book --dry-run` / `quote` are the chargeable truth.
**Default option order is the server's value ranking**, a composite of price / stops / duration computed server-side — it is NOT sorted by price. `topOptions[0]` (and `options[0]`) is the server's value pick, not the cheapest. To rank by a single factual field, pass `--sort price|duration|stops` (a client-side sort of the returned options); callers that specifically want the cheapest must sort or filter by `price` explicitly.
**Re-searching a goal reuses its existing selection.** Running `search` again on a goal that already has a selection reuses that selection rather than creating a new one, so a re-search with DIFFERENT dates can return results computed for the original parameters. Always verify the effective dates in the response before selecting — cross-reference the echoed search params (`--full` includes the per-option data) rather than assuming the new dates took effect.
### Cart + Book (Style A JSON)

@@ -513,2 +525,4 @@ ```bash

>
> 👥 **Search prices are party totals.** Every search/selection option price is the total for the searched traveller group, NOT per-person — do not multiply by traveller count (hotel search prices are additionally whole-STAY totals, not nightly). `quote`'s `chargeableTotal` and `book --dry-run`'s `chargeableSubtotal` are the chargeable truth.
>
> ✉️ **`send` is not idempotent** — every invocation emails the client again. Non-interactive runs refuse without `--yes` (`CONFIRMATION_REQUIRED`). Send once; track with `plan-status`.

@@ -515,0 +529,0 @@

import chalk from "chalk";
import { createInterface } from "readline/promises";
import { stdin, stdout } from "process";
import { openBrowser } from "../utils.js";
import { openBrowser, maskLoyaltyValue } from "../utils.js";
import { saveCredentials, getToken, getApiUrl, clearCredentials, credentialsExist, saveUserContext, getUserContext } from "../config.js";
import { graphql } from "../api.js";
import { UPDATE_MY_USER } from "../queries.js";
import { CliError, CliErrorCode, authFailedMessage } from "../errors.js";

@@ -54,10 +55,10 @@ const CITY_AIRPORTS = {

}
function maskNumber(num, showLast = 4) {
if (num.length <= showLast)
return num;
return "••••" + num.slice(-showLast);
function formatPassport(p) {
const bits = [`••••${p.last4}`];
if (p.issueCountry)
bits.push(p.issueCountry);
if (p.expirationDate)
bits.push(`exp ${p.expirationDate}`);
return bits.join(" · ");
}
function maskFFPrograms(programs) {
return programs.map((ff) => ({ airlineCode: ff.airlineCode, membershipNumber: maskNumber(ff.membershipNumber) }));
}
async function prompt(rl, question) {

@@ -67,2 +68,48 @@ const answer = await rl.question(question);

}
async function promptMuted(rl, question) {
const iface = rl;
const origWriteToOutput = iface._writeToOutput;
const canMute = typeof origWriteToOutput === "function" && !!iface.output;
let muted = false;
if (canMute) {
iface._writeToOutput = (stringToWrite) => {
if (!muted)
iface.output.write(stringToWrite);
};
}
try {
const pending = rl.question(question);
muted = true;
let answer = "";
try {
answer = await pending;
}
catch {
answer = "";
}
return answer.trim();
}
finally {
if (canMute) {
iface._writeToOutput = origWriteToOutput;
iface.output.write("\n");
}
}
}
async function readPassportNumber(rl, promptText) {
for (let attempt = 0; attempt < 2; attempt++) {
const raw = (await promptMuted(rl, promptText)).toUpperCase();
if (!raw)
return null;
if (/^[A-Z0-9]{6,9}$/.test(raw))
return raw;
if (attempt === 0) {
console.log(chalk.yellow(" ⚠ A passport number is 6–9 letters or digits. Let's try that again."));
}
else {
console.log(chalk.yellow(" ⚠ That still isn't a valid passport number."));
}
}
return null;
}
async function readStdin() {

@@ -106,4 +153,10 @@ const chunks = [];

if (me.frequentFlyerPrograms && me.frequentFlyerPrograms.length > 0) {
ctx.frequentFlyerPrograms = maskFFPrograms(me.frequentFlyerPrograms);
ctx.frequentFlyerPrograms = me.frequentFlyerPrograms.map((ff) => ({
airlineCode: ff.airlineCode,
membershipNumber: ff.membershipNumber,
}));
}
if (existingCtx?.hotelLoyaltyPrograms && existingCtx.hotelLoyaltyPrograms.length > 0) {
ctx.hotelLoyaltyPrograms = existingCtx.hotelLoyaltyPrograms;
}
return { ctx, me };

@@ -135,5 +188,9 @@ }

if (me.frequentFlyerPrograms && me.frequentFlyerPrograms.length > 0) {
const ffs = me.frequentFlyerPrograms.map(ff => `${ff.airlineCode} ${maskNumber(ff.membershipNumber)}`).join(", ");
const ffs = me.frequentFlyerPrograms.map(ff => `${ff.airlineCode} ${maskLoyaltyValue(ff.membershipNumber)}`).join(", ");
parts.push(` ✈️ ${ffs}`);
}
if (ctx.hotelLoyaltyPrograms && ctx.hotelLoyaltyPrograms.length > 0) {
const hls = ctx.hotelLoyaltyPrograms.map(hl => `${hl.chainCode} ${maskLoyaltyValue(hl.membershipNumber)}`).join(", ");
parts.push(` 🏨 ${hls}`);
}
if (parts.length > 0) {

@@ -207,5 +264,9 @@ console.log(parts.join("\n"));

if (ctx.frequentFlyerPrograms && ctx.frequentFlyerPrograms.length > 0) {
const ffs = ctx.frequentFlyerPrograms.map(ff => `${ff.airlineCode} ${maskNumber(ff.membershipNumber)}`).join(", ");
const ffs = ctx.frequentFlyerPrograms.map(ff => `${ff.airlineCode} ${maskLoyaltyValue(ff.membershipNumber)}`).join(", ");
console.log(` ✈️ FF: ${ffs}`);
}
if (ctx.hotelLoyaltyPrograms && ctx.hotelLoyaltyPrograms.length > 0) {
const hls = ctx.hotelLoyaltyPrograms.map(hl => `${hl.chainCode} ${maskLoyaltyValue(hl.membershipNumber)}`).join(", ");
console.log(` 🏨 Hotel: ${hls}`);
}
}

@@ -228,3 +289,3 @@ else {

console.log(" Generate a Personal Access Token at:\n");
console.log(chalk.cyan(` ${webUrl}/settings\n`));
console.log(chalk.cyan(` ${webUrl}/me/settings/tokens\n`));
console.log(" Then run:\n");

@@ -234,3 +295,3 @@ console.log(chalk.cyan(" voyagier auth set-token <your-token>\n"));

}
const settingsUrl = `${webUrl}/settings`;
const settingsUrl = `${webUrl}/me/settings/tokens`;
console.log(` 1. Go to ${chalk.cyan(settingsUrl)}`);

@@ -277,3 +338,3 @@ console.log(` 2. Generate a Personal Access Token`);

.command("setup")
.description("Configure your traveller profile (airports, cabin, passport, frequent flyer)")
.description("Configure your traveller profile (airports, cabin, passport, frequent flyer, hotel loyalty)")
.option("--airports <codes>", "Home airport(s), comma-separated (e.g. BWI,DCA,IAD)")

@@ -402,23 +463,31 @@ .option("--cabin <class>", "Preferred cabin: economy, premium_economy, business, first")

}
let passportToSync;
let ffToSync;
if (!opts.skipPassport) {
console.log(` 🛂 ${chalk.bold("Passport")}`);
if (me.passport) {
console.log(chalk.dim(` On file: ••••${me.passport.last4} (${me.passport.issueCountry}, exp ${me.passport.expirationDate})`));
userCtx.passport = me.passport;
console.log(chalk.green(" ✓ Imported from profile\n"));
const onFile = me.passport;
if (onFile) {
console.log(chalk.dim(` On file: ${formatPassport(onFile)}`));
}
else if (rl) {
const last4 = await prompt(rl, " Passport number (last 4 digits, or Enter to skip): ");
if (last4 && /^\d{4}$/.test(last4)) {
const issueCountry = await prompt(rl, " Issue country (e.g. US): ") || "US";
const nationality = await prompt(rl, " Nationality (e.g. US): ") || issueCountry;
const expiration = await prompt(rl, " Expiration (YYYY-MM): ");
userCtx.passport = {
last4,
issueCountry: issueCountry.toUpperCase(),
nationalityCountry: nationality.toUpperCase(),
expirationDate: expiration || "unknown",
};
console.log(chalk.green(` ✓ ••••${last4} (${userCtx.passport.issueCountry}, exp ${userCtx.passport.expirationDate})\n`));
if (rl) {
const promptText = onFile
? " Enter to keep, or type a new number to replace (never displayed): "
: " Passport number (sent securely to your profile, never displayed — or Enter to skip): ";
const number = await readPassportNumber(rl, promptText);
if (number) {
const defIssue = onFile?.issueCountry ?? "US";
const defNat = onFile?.nationalityCountry;
const defExp = onFile?.expirationDate;
const issueCountry = ((await prompt(rl, ` Issue country (e.g. US) [${defIssue}]: `)) || defIssue).toUpperCase();
const nationality = ((await prompt(rl, ` Nationality (e.g. US)${defNat ? ` [${defNat}]` : ""}: `)) || defNat || issueCountry).toUpperCase();
const expiration = (await prompt(rl, ` Expiration (YYYY-MM)${defExp ? ` [${defExp}]` : ""}: `)) || defExp || "";
passportToSync = { passportNumber: number, issueCountry, nationalityCountry: nationality };
if (expiration)
passportToSync.expirationDate = expiration;
console.log(chalk.green(` ✓ ${formatPassport({ last4: number.slice(-4), issueCountry, expirationDate: expiration || undefined })}\n`));
}
else if (onFile) {
userCtx.passport = onFile;
console.log(chalk.dim(" Keeping the passport on file.\n"));
}
else {

@@ -428,2 +497,6 @@ console.log(chalk.dim(" Skipped.\n"));

}
else if (onFile) {
userCtx.passport = onFile;
console.log(chalk.green(" ✓ Imported from profile\n"));
}
else {

@@ -436,4 +509,3 @@ console.log(chalk.dim(" Skipped (non-interactive).\n"));

if (me.frequentFlyerPrograms && me.frequentFlyerPrograms.length > 0) {
userCtx.frequentFlyerPrograms = maskFFPrograms(me.frequentFlyerPrograms);
const display = me.frequentFlyerPrograms.map(ff => `${ff.airlineCode} ${maskNumber(ff.membershipNumber)}`).join(", ");
const display = me.frequentFlyerPrograms.map(ff => `${ff.airlineCode} ${maskLoyaltyValue(ff.membershipNumber)}`).join(", ");
console.log(chalk.dim(` On file: ${display}`));

@@ -458,4 +530,4 @@ console.log(chalk.green(" ✓ Imported from profile\n"));

if (/^[A-Z0-9]{2}$/.test(airline)) {
programs.push({ airlineCode: airline, membershipNumber: maskNumber(number) });
console.log(chalk.green(` ✓ ${airline} ${maskNumber(number)}`));
programs.push({ airlineCode: airline, membershipNumber: number });
console.log(chalk.green(` ✓ ${airline} ${maskLoyaltyValue(number)}`));
}

@@ -472,3 +544,3 @@ else {

if (programs.length > 0) {
userCtx.frequentFlyerPrograms = programs;
ffToSync = programs;
}

@@ -481,2 +553,73 @@ console.log();

}
console.log(` 🏨 ${chalk.bold("Hotel Loyalty")}`);
const existingHotel = userCtx.hotelLoyaltyPrograms ?? [];
if (existingHotel.length > 0) {
const display = existingHotel.map(hl => `${hl.chainCode} ${maskLoyaltyValue(hl.membershipNumber)}`).join(", ");
console.log(chalk.dim(` On file: ${display}`));
}
if (rl) {
const programs = [];
let adding = true;
while (adding) {
const hlInput = await prompt(rl, programs.length === 0
? " Add a program (e.g. HI 12345678, or Enter to skip): "
: " Add another (or Enter to finish): ");
if (!hlInput) {
adding = false;
}
else {
const parts = hlInput.split(/\s+/);
if (parts.length >= 2) {
const chain = parts[0].toUpperCase();
const number = parts.slice(1).join("");
if (!/^[A-Z]{2}$/.test(chain)) {
console.log(chalk.yellow(` ⚠ Invalid chain code "${chain}" (expected 2 letters)`));
}
else if (!/^\d+$/.test(number)) {
console.log(chalk.yellow(" ⚠ Member number must be digits only (the chain code is added at booking time)"));
}
else {
programs.push({ chainCode: chain, membershipNumber: number });
console.log(chalk.green(` ✓ ${chain} ${maskLoyaltyValue(number)}`));
}
}
else {
console.log(chalk.yellow(" ⚠ Format: CHAIN NUMBER (e.g. HI 12345678)"));
}
}
}
if (programs.length > 0) {
userCtx.hotelLoyaltyPrograms = programs;
}
console.log();
}
else if (existingHotel.length === 0) {
console.log(chalk.dim(" Skipped (non-interactive).\n"));
}
else {
console.log();
}
if (passportToSync || ffToSync) {
const updateInput = {};
if (passportToSync)
updateInput.passport = passportToSync;
if (ffToSync)
updateInput.frequentFlyerPrograms = ffToSync;
try {
const data = await graphql(UPDATE_MY_USER, { input: updateInput });
const updated = data.updateMyUser;
if (passportToSync)
userCtx.passport = updated.passport ?? undefined;
if (ffToSync && updated.frequentFlyerPrograms) {
userCtx.frequentFlyerPrograms = updated.frequentFlyerPrograms.map(ff => ({
airlineCode: ff.airlineCode,
membershipNumber: ff.membershipNumber,
}));
}
}
catch {
console.log(chalk.red(" ✗ Profile sync failed — your local setup is saved."));
console.log(chalk.dim(" Run `voyagier auth setup` again later or check `voyagier doctor`.\n"));
}
}
saveUserContext(userCtx);

@@ -492,7 +635,11 @@ console.log(chalk.dim(" ──────────────────"));

if (userCtx.passport)
console.log(` Passport: ••••${userCtx.passport.last4} (${userCtx.passport.issueCountry}, exp ${userCtx.passport.expirationDate})`);
console.log(` Passport: ${formatPassport(userCtx.passport)}`);
if (userCtx.frequentFlyerPrograms && userCtx.frequentFlyerPrograms.length > 0) {
const ffs = userCtx.frequentFlyerPrograms.map(ff => `${ff.airlineCode} ${maskNumber(ff.membershipNumber)}`).join(", ");
const ffs = userCtx.frequentFlyerPrograms.map(ff => `${ff.airlineCode} ${maskLoyaltyValue(ff.membershipNumber)}`).join(", ");
console.log(` FF: ${ffs}`);
}
if (userCtx.hotelLoyaltyPrograms && userCtx.hotelLoyaltyPrograms.length > 0) {
const hls = userCtx.hotelLoyaltyPrograms.map(hl => `${hl.chainCode} ${maskLoyaltyValue(hl.membershipNumber)}`).join(", ");
console.log(` Hotel: ${hls}`);
}
console.log(chalk.dim(`\n Search flights: voyagier search flights --to SJU --date 2026-05-14\n`));

@@ -499,0 +646,0 @@ }

@@ -6,7 +6,9 @@ import chalk from "chalk";

import { formatPrice, openBrowser, deriveBaseUrl, shellArg, cents } from "../utils.js";
import { clientPlanUrl, planUrls } from "../plan-urls.js";
import { resolvePlanArg } from "../resolve-plan-arg.js";
import { hintCheckoutCreated, hintBookingConfirmed, hintBookingPending, hintDryRun } from "../hints.js";
import { GET_CART_V2, CREATE_CHECKOUT, GET_PAYMENT_CHECKOUTS } from "../queries.js";
import { GET_CART_V2, CREATE_CHECKOUT, GET_PAYMENT_CHECKOUTS, GET_PLAN_STATUS } from "../queries.js";
import { collectBlockers, filterBookable, filterByTypes, } from "./cart-helpers.js";
import { buildCheckoutPreview } from "./checkout-preview.js";
import { buildPlanStatus, resolveHotelCodes, } from "./plan-status.js";
export function parseMoney(raw, flagName) {

@@ -30,2 +32,60 @@ const cleaned = raw.trim().replace(/^\$/, "");

}
export function blockerFix(b, travellers) {
switch (b.kind) {
case "TRAVELLER_DATA": {
const missing = travellers.find((t) => t.travellerId === b.refs.travellerId)?.missing ?? [];
const flags = [
missing.includes("gender") ? "--gender <M|F|X>" : null,
missing.includes("dateOfBirth") ? "--dob <YYYY-MM-DD>" : null,
missing.includes("passport")
? "--passport-number <number> --passport-country <code> --passport-expiry <YYYY-MM>"
: null,
].filter(Boolean);
return `voyagier travellers update ${shellArg(b.refs.travellerId ?? "")} ${flags.join(" ")}`.trim();
}
case "PICK_PENDING": {
const single = b.candidateSelectionIds && b.candidateSelectionIds.length === 1
? b.candidateSelectionIds[0]
: b.refs.selectionId;
if (single && !(b.candidateSelectionIds && b.candidateSelectionIds.length > 1)) {
return `voyagier select --selection-id ${shellArg(single)} --option-id <optionId>`;
}
return `voyagier plans goal ${shellArg(b.refs.goalId ?? "")} --json`;
}
case "SELECTION_INPUT":
case "REQUIREMENT_UNMET":
default:
return `voyagier plans goal ${shellArg(b.refs.goalId ?? "")} --json`;
}
}
async function enforcePlanReadiness(planId, baseUrl, planIdArg) {
let statusData;
try {
statusData = await graphql(GET_PLAN_STATUS, { id: planId });
}
catch (err) {
const note = "Could not verify the plan's readiness before checkout — refusing to book " +
`(re-run with --force-checkout to trust the server's own validation, or check: voyagier plan-status ${planIdArg}).`;
if (err instanceof CliError)
throw new CliError(err.code, `${note}\n${err.message}`, err.details);
const message = err instanceof Error ? err.message : String(err);
throw new CliError(CliErrorCode.API_ERROR, `${note}\n${message}`);
}
if (!statusData.tripPlan)
return;
const hotelCodes = await resolveHotelCodes(statusData);
const status = buildPlanStatus(statusData, baseUrl, hotelCodes);
const hardBlockers = status.blockers.filter((b) => b.unverified !== true);
if (hardBlockers.length === 0)
return;
const detailed = hardBlockers.map((b) => ({
kind: b.kind,
message: b.message,
refs: b.refs,
fix: blockerFix(b, status.travellers),
}));
const lines = detailed.map((b) => ` • [${b.kind}] ${b.message}\n fix: ${b.fix}`);
throw new CliError(CliErrorCode.PLAN_BLOCKED, `Cannot book — the plan has ${hardBlockers.length} unresolved blocker${hardBlockers.length === 1 ? "" : "s"} that must be resolved before checkout ` +
`(or pass --force-checkout to trust the server's own validation):\n${lines.join("\n")}`, { blockers: detailed });
}
export function registerBookCommands(program) {

@@ -44,2 +104,3 @@ program

.option("--rebook", "Create a checkout even though a Paid checkout already exists for this plan")
.option("--force-checkout", "Skip the client-side readiness guard and trust the server's own validation")
.option("--status", "Show payment + booking status for past checkouts on this plan")

@@ -50,3 +111,3 @@ .option("--plan <id>", "Trip plan ID (alternative to the positional argument)")

const baseUrl = deriveBaseUrl(getApiUrl());
const planUrl = `${baseUrl}/plans/${planId}`;
const planUrl = clientPlanUrl(planId, baseUrl);
if (opts.status) {

@@ -107,3 +168,3 @@ await showBookingStatus(planId, baseUrl, Boolean(opts.json), Boolean(opts.agent));

title: plan.title,
url: planUrl,
...planUrls(plan.id, baseUrl),
urlForCli: `voyagier plans get ${shellArg(plan.id)}`,

@@ -263,2 +324,5 @@ };

}
if (!opts.forceCheckout) {
await enforcePlanReadiness(planId, baseUrl, planIdArg);
}
if (!opts.json && !opts.agent) {

@@ -350,3 +414,3 @@ process.stderr.write(chalk.dim("Creating checkout session...\n"));

const checkouts = data.tripPlanPaymentCheckouts ?? [];
const planUrl = `${baseUrl}/plans/${planId}`;
const planUrl = clientPlanUrl(planId, baseUrl);
if (json) {

@@ -360,3 +424,3 @@ const renamed = checkouts.map((c) => ({

data: { checkouts: renamed },
planContext: { planId, url: planUrl, urlForCli: `voyagier plans get ${shellArg(planId)}` },
planContext: { planId, ...planUrls(planId, baseUrl), urlForCli: `voyagier plans get ${shellArg(planId)}` },
}, null, 2) + "\n");

@@ -363,0 +427,0 @@ return;

@@ -6,2 +6,3 @@ import chalk from "chalk";

import { getApiUrl } from "../config.js";
import { clientPlanUrl, planUrls } from "../plan-urls.js";
import { GET_BOOKING_RECORDS, GET_BOOKING_RECORDS_BY_USER, REFRESH_BOOKING_RECORD, GET_BOOKING_RECORD, } from "../queries.js";

@@ -69,3 +70,3 @@ function statusIcon(status) {

amountCents: amount,
...(r.tripPlanId ? { url: `${baseUrl}/plans/${r.tripPlanId}` } : {}),
...(r.tripPlanId ? planUrls(r.tripPlanId, baseUrl) : {}),
}));

@@ -123,3 +124,3 @@ process.stdout.write(JSON.stringify({ bookings: enriched }, null, 2) + "\n");

amountCents: amount,
...(r.tripPlanId ? { url: `${baseUrl}/plans/${r.tripPlanId}` } : {}),
...(r.tripPlanId ? planUrls(r.tripPlanId, baseUrl) : {}),
};

@@ -149,3 +150,3 @@ process.stdout.write(JSON.stringify(enriched, null, 2) + "\n");

console.log(` Plan: ${r.tripPlan.title}`);
console.log(chalk.dim(` ${baseUrl}/plans/${r.tripPlan.id}`));
console.log(chalk.dim(` ${clientPlanUrl(r.tripPlan.id, baseUrl)}`));
}

@@ -152,0 +153,0 @@ const item = r.tripPlanPlace ?? r.tripPlanProduct;

@@ -6,2 +6,3 @@ import chalk from "chalk";

import { formatPrice, deriveBaseUrl, shellArg } from "../utils.js";
import { clientPlanUrl, planUrls } from "../plan-urls.js";
import { resolvePlanArg } from "../resolve-plan-arg.js";

@@ -20,3 +21,3 @@ import { GET_CART_V2 } from "../queries.js";

const baseUrl = deriveBaseUrl(getApiUrl());
const planUrl = `${baseUrl}/plans/${planId}`;
const planUrl = clientPlanUrl(planId, baseUrl);
let data;

@@ -43,3 +44,3 @@ try {

title: plan.title,
url: planUrl,
...planUrls(plan.id, baseUrl),
urlForCli: `voyagier plans get ${shellArg(plan.id)}`,

@@ -46,0 +47,0 @@ };

@@ -6,5 +6,5 @@ import chalk from "chalk";

import { GET_TRIP_PLAN_EVENTS } from "../queries.js";
import { validateDate, deriveBaseUrl } from "../utils.js";
import { validateDate } from "../utils.js";
import { resolvePlanArg } from "../resolve-plan-arg.js";
import { getApiUrl } from "../config.js";
import { clientPlanUrl, planUrls } from "../plan-urls.js";
export function computeDayNumber(eventDatetime, planStart) {

@@ -84,3 +84,3 @@ if (!eventDatetime || !planStart)

function planUrl(planId) {
return `${deriveBaseUrl(getApiUrl())}/plans/${planId}`;
return clientPlanUrl(planId);
}

@@ -152,3 +152,3 @@ function formatEventLine(e, planStart) {

title: plan.title,
url: planUrl(plan.id),
...planUrls(plan.id),
startDate: plan.startDate ?? null,

@@ -155,0 +155,0 @@ endDate: plan.endDate ?? null,

@@ -7,2 +7,3 @@ import chalk from "chalk";

import { deriveBaseUrl, formatPrice, shellArg } from "../utils.js";
import { planUrls } from "../plan-urls.js";
import { resolvePlanArg } from "../resolve-plan-arg.js";

@@ -426,3 +427,3 @@ import { GET_PLAN_STATUS, GET_HOTEL_OPTION_DATA } from "../queries.js";

title: plan.title ?? null,
url: `${planUrlBase}/plans/${plan.id}`,
...planUrls(plan.id, planUrlBase),
readiness,

@@ -429,0 +430,0 @@ summary: {

@@ -6,2 +6,3 @@ import chalk from "chalk";

import { validateDate, warnPastDate, validateIata, deriveBaseUrl, shellArg } from "../utils.js";
import { clientPlanUrl, planUrls } from "../plan-urls.js";
import { progress, warn, fatal, jsonOutput } from "../output.js";

@@ -190,3 +191,3 @@ import { CliError, CliErrorCode } from "../errors.js";

const baseUrl2 = deriveBaseUrl(getApiUrl());
const planUrl = `${baseUrl2}/plans/${plan.id}`;
const planUrl = clientPlanUrl(plan.id, baseUrl2);
const nextSteps = [];

@@ -219,3 +220,3 @@ if (opts.to && opts.depart) {

note: "plan-trip creates a starting plan + default goal graph (a round-trip + hotel TEMPLATE); compose the trip with the primitives below. Prune goals your brief doesn't need — shape flags (--one-way/--flight-only/--hotel-only) at scaffold time, or `plans goal-remove <goalId> --force` any time.",
url: planUrl,
...planUrls(plan.id, baseUrl2),
...(shapeLabels.length > 0

@@ -222,0 +223,0 @@ ? {

@@ -6,2 +6,3 @@ import chalk from "chalk";

import { formatPrice, deriveBaseUrl, shellArg } from "../../utils.js";
import { clientPlanUrl, planUrls } from "../../plan-urls.js";
import { resolvePlanArg } from "../../resolve-plan-arg.js";

@@ -20,3 +21,3 @@ import { GET_CART_V2 } from "../../queries.js";

const baseUrl = deriveBaseUrl(getApiUrl());
const planUrl = `${baseUrl}/plans/${planId}`;
const planUrl = clientPlanUrl(planId, baseUrl);
let data;

@@ -45,3 +46,3 @@ try {

title: plan.title,
url: planUrl,
...planUrls(plan.id, baseUrl),
urlForCli: `voyagier plans get ${shellArg(plan.id)}`,

@@ -48,0 +49,0 @@ };

@@ -11,2 +11,3 @@ import chalk from "chalk";

import { planUrl, typeIcon, chosenOption } from "./types.js";
import { planUrls } from "../../plan-urls.js";
import { GET_TRIP_PLANS, GET_TRIP_PLAN, GET_TRIP_PLAN_SUMMARY, UPDATE_TRIP_PLAN, GET_TRIP_PLAN_WITH_DESC, DELETE_TRIP_PLAN, } from "../../queries.js";

@@ -45,3 +46,3 @@ export function registerCrudCommands(plans) {

const planSummary = await getPlanSummary(plan.id);
jsonOutput({ ...plan, url: planUrl(plan.id), planSummary });
jsonOutput({ ...plan, ...planUrls(plan.id), planSummary });
return;

@@ -106,3 +107,3 @@ }

process.stdout.write(JSON.stringify({
items: items.map((p) => ({ ...p, url: planUrl(p.id) })),
items: items.map((p) => ({ ...p, ...planUrls(p.id) })),
total: opts.active ? items.length : total,

@@ -173,3 +174,3 @@ page: opts.active ? 1 : page,

if (opts.json) {
process.stdout.write(JSON.stringify({ ...plan, url: planUrl(plan.id) }, null, 2) + "\n");
process.stdout.write(JSON.stringify({ ...plan, ...planUrls(plan.id) }, null, 2) + "\n");
return;

@@ -176,0 +177,0 @@ }

@@ -5,2 +5,3 @@ import chalk from "chalk";

import { deriveBaseUrl, formatDateRange } from "../../utils.js";
import { clientPlanUrl } from "../../plan-urls.js";
import { resolvePlanArg } from "../../resolve-plan-arg.js";

@@ -162,3 +163,3 @@ import { jsonOutput } from "../../output.js";

console.log(` ${chalk.white(p.title)}${dates}`);
console.log(chalk.dim(` ${baseUrl}/plans/${p.id}`));
console.log(chalk.dim(` ${clientPlanUrl(p.id, baseUrl)}`));
}

@@ -165,0 +166,0 @@ console.log();

@@ -1,3 +0,2 @@

import { getApiUrl } from "../../config.js";
import { deriveBaseUrl } from "../../utils.js";
import { clientPlanUrl } from "../../plan-urls.js";
import { deriveChosen } from "../../choices.js";

@@ -29,4 +28,3 @@ export function deepChosenOption(sel) {

export function planUrl(id) {
const baseUrl = deriveBaseUrl(getApiUrl());
return `${baseUrl}/plans/${id}`;
return clientPlanUrl(id);
}

@@ -33,0 +31,0 @@ export function inferTypeFromTitle(title) {

@@ -164,1 +164,30 @@ import { graphql } from "../api.js";

}
const PARAM_LABELS = {
origin: "origin",
destination: "destination",
depart: "departure date",
return: "return date",
checkin: "check-in",
checkout: "check-out",
partySize: "party size",
};
export function diffSearchParams(effective, requested) {
const norm = (v) => (v === undefined || v === null ? undefined : v);
const changed = [];
for (const key of Object.keys(PARAM_LABELS)) {
const a = norm(effective[key]);
const b = norm(requested[key]);
if (a !== b)
changed.push(key);
}
return changed;
}
export function formatReuseWarning(changed, effective, requested) {
const parts = changed.map((key) => {
const from = effective[key];
const to = requested[key];
return `${PARAM_LABELS[key]} ${from ?? "—"} → ${to ?? "—"}`;
});
return (`SELECTION_REUSED_PARAMS_MISMATCH: this search reused an existing selection whose inventory ` +
`was fetched for different params (${parts.join(", ")}); the results may reflect the original search params, not the ones just requested.`);
}
import { printPlanFooter } from "../plan-footer.js";
import chalk from "chalk";
import { graphql } from "../api.js";
import { getApiUrl, getHomeAirports } from "../config.js";
import { getHomeAirports } from "../config.js";
import { GET_TRAVELLERS_BRIEF, CREATE_FLIGHT_SELECTION, GET_TRIP_PLAN_ITEM_TYPES, DELETE_TRIP_PLAN_ITEM, CREATE_HOTEL_SELECTION, CREATE_ACTIVITY_SELECTION, GET_DECISION_SELECTION_OPTIONS, } from "../queries.js";
import { loadGoals, resolveGoal, resolveMirrorList, resolveDecisionSelection, setAirport, addDateOption, resolveDateRange, requireAirports, resolveReturnFlightGoal, requireDateSelection, setDestination, } from "./search-helpers.js";
import { saveSearchState, loadSearchState } from "../state.js";
import { loadGoals, resolveGoal, resolveMirrorList, resolveDecisionSelection, setAirport, addDateOption, resolveDateRange, requireAirports, resolveReturnFlightGoal, requireDateSelection, setDestination, diffSearchParams, formatReuseWarning, } from "./search-helpers.js";
import { saveSearchState, loadSearchState, getSelectionSearchParams, rememberSelectionSearchParams } from "../state.js";
import { formatFlights, formatHotels, formatActivities } from "../formatters.js";
import { extractFlightToken, buildFlightSummary, buildHotelSummary, buildActivitySummary, validateDate, warnPastDate, validateIata, deriveBaseUrl, looksLikeAirportCode, shellArg } from "../utils.js";
import { extractFlightToken, buildFlightSummary, buildHotelSummary, buildActivitySummary, validateDate, warnPastDate, validateIata, looksLikeAirportCode, shellArg } from "../utils.js";
import { clientPlanUrl, planUrls } from "../plan-urls.js";
import { agentFlightOptions, agentHotelOptions, agentActivityOptions } from "../agent-output.js";

@@ -15,3 +16,4 @@ import { deriveHotelStay } from "../hotel-format.js";

import { CliError, CliErrorCode } from "../errors.js";
import { startSpinner } from "../spinner.js";
import { waitForSelectionOptions } from "../selection-wait.js";
import { startSpinner, spinnerAnimates } from "../spinner.js";
import { isInteractive, promptText } from "../prompt.js";

@@ -177,2 +179,72 @@ import { scaffoldPlan, generateTripTitle } from "./scaffold.js";

}
const SEARCH_WAIT_TIMEOUT_MS = 90_000;
const WAIT_HEARTBEAT_MS = 10_000;
function makeWaitHeartbeat(label, waitSpinner) {
let lastBucket = 0;
return ({ elapsedMs }) => {
const line = `${label}… fetching inventory (${Math.round(elapsedMs / 1000)}s)`;
if (waitSpinner) {
waitSpinner.update(line);
return;
}
const bucket = Math.floor(elapsedMs / WAIT_HEARTBEAT_MS);
if (bucket > lastBucket) {
lastBucket = bucket;
process.stderr.write(chalk.dim(line + "\n"));
}
};
}
async function refetchDecisionOptions(selectionId) {
const data = await graphql(GET_DECISION_SELECTION_OPTIONS, { tripPlanSelectionId: selectionId });
return data.getTripPlanSelection?.options ?? [];
}
function shouldWaitInline(opts) {
return (!opts.json &&
!opts.agent &&
opts.wait !== false &&
process.stderr.isTTY === true);
}
function writePollHint(selectionId) {
process.stderr.write(chalk.dim("Poll for results with:\n"));
process.stderr.write(chalk.dim(` voyagier selection-options ${shellArg(selectionId)} --wait\n`));
}
function reportWaitStop(result, selectionId) {
switch (result.status) {
case "FETCHING":
process.stderr.write(chalk.yellow("Inventory is still loading on our side — your results will be ready shortly.\n"));
writePollHint(selectionId);
break;
case "FETCH_ERROR":
process.stderr.write(chalk.yellow(`The inventory search hit an error while fetching${result.fetchError ? `: ${result.fetchError}` : "."}\n`));
writePollHint(selectionId);
break;
case "AWAITING_INPUT":
process.stderr.write(chalk.yellow("The search is missing a required input, so no inventory could be fetched. Check the owning goal: voyagier plans goal <goalId>\n"));
break;
case "NO_RESULTS":
default:
break;
}
}
function observeSelectionReuse(selectionId, requested) {
const stored = getSelectionSearchParams(selectionId);
if (!stored) {
rememberSelectionSearchParams(selectionId, requested);
return { requestedParams: requested, warnings: [] };
}
const changed = diffSearchParams(stored, requested);
const warnings = changed.length > 0 ? [formatReuseWarning(changed, stored, requested)] : [];
return { requestedParams: requested, effectiveParams: stored, warnings };
}
function reuseEnvelopeFields(obs) {
return {
requestedParams: obs.requestedParams,
...(obs.effectiveParams ? { effectiveParams: obs.effectiveParams } : {}),
...(obs.warnings.length > 0 ? { warnings: obs.warnings } : {}),
};
}
function writeReuseWarnings(warnings) {
for (const w of warnings)
process.stderr.write(chalk.yellow(`⚠ ${w}\n`));
}
export function registerSearchCommands(program) {

@@ -230,2 +302,3 @@ const search = program.command("search").description("Search flights, hotels, and activities");

.option("--dry-run", "Show the GraphQL query without executing")
.option("--no-wait", "Return immediately instead of waiting inline for async inventory (human/TTY mode)")
.option("--no-input", "Never prompt for missing input; fail instead (for scripts, agents, CI)")

@@ -319,2 +392,31 @@ .action(async (opts, command) => {

}
const reuse = observeSelectionReuse(selectionId, {
origin,
destination,
depart: opts.date,
...(opts.return ? { return: opts.return } : {}),
partySize: travellerIds.length,
});
if (fetchedOptions.length === 0 && shouldWaitInline(opts)) {
const label = `Searching ${origin} → ${destination}`;
const waitSpinner = spinnerAnimates() ? startSpinner(`${label}… fetching inventory`) : null;
let snap;
try {
snap = await waitForSelectionOptions(selectionId, { timeoutMs: SEARCH_WAIT_TIMEOUT_MS }, { heartbeat: makeWaitHeartbeat(label, waitSpinner) });
}
finally {
waitSpinner?.stop();
}
if (snap.result.status === "READY") {
fetchedOptions = await refetchDecisionOptions(selectionId);
}
else if (snap.result.status === "NO_RESULTS") {
process.stderr.write(chalk.yellow(`No flights matched ${origin} → ${destination} on these dates.\n`));
return;
}
else {
reportWaitStop(snap.result, selectionId);
return;
}
}
const sortBy = (opts.sort ?? "default");

@@ -354,3 +456,4 @@ let filtered = [...fetchedOptions].sort((a, b) => a.sortOrder - b.sortOrder);

isRoundTrip,
url: `${deriveBaseUrl(getApiUrl())}/plans/${tripPlanId}`,
...planUrls(tripPlanId),
...reuseEnvelopeFields(reuse),
}, options, searchResults, opts.full, "--sort/--max-stops"), null, 2) + "\n");

@@ -360,3 +463,3 @@ return;

if (opts.agent) {
const planUrl = `${deriveBaseUrl(getApiUrl())}/plans/${tripPlanId}`;
const planUrl = clientPlanUrl(tripPlanId);
const lines = [];

@@ -366,2 +469,4 @@ lines.push(`### Flights (${origin} → ${destination})`);

lines.push(`_No plan given — created draft plan \`${tripPlanId}\`._`);
for (const w of reuse.warnings)
lines.push(`> ⚠ ${w}`);
if (options.length === 0) {

@@ -390,5 +495,6 @@ lines.push("_No options yet — the search is still fetching inventory._");

}
writeReuseWarnings(reuse.warnings);
if (options.length === 0) {
process.stderr.write(chalk.dim("No options yet — the search is still fetching inventory.\n"));
process.stderr.write(chalk.dim(` Poll: voyagier selection-options ${shellArg(selectionId)} --wait\n`));
writePollHint(selectionId);
return;

@@ -427,2 +533,3 @@ }

.option("--verbose", "Show request details sent to the API")
.option("--no-wait", "Return immediately instead of waiting inline for async inventory (human/TTY mode)")
.option("--no-input", "Never prompt for missing input; fail instead (for scripts, agents, CI)")

@@ -515,2 +622,26 @@ .action(async (opts) => {

}
const reuse = observeSelectionReuse(selectionId, {
destination: opts.location,
checkin: opts.checkin,
checkout: opts.checkout,
partySize: adults,
});
if (fetchedOptions.length === 0 && shouldWaitInline(opts)) {
const label = `Searching hotels in ${opts.location}`;
const waitSpinner = spinnerAnimates() ? startSpinner(`${label}… fetching inventory`) : null;
let snap;
try {
snap = await waitForSelectionOptions(selectionId, { timeoutMs: SEARCH_WAIT_TIMEOUT_MS }, { heartbeat: makeWaitHeartbeat(label, waitSpinner) });
}
finally {
waitSpinner?.stop();
}
if (snap.result.status === "READY") {
fetchedOptions = await refetchDecisionOptions(selectionId);
}
else if (snap.result.status !== "NO_RESULTS") {
reportWaitStop(snap.result, selectionId);
return;
}
}
const sortBy = (opts.sort ?? "default");

@@ -549,3 +680,4 @@ const options = sortBy === "price"

selectionId: selectionId,
url: `${deriveBaseUrl(getApiUrl())}/plans/${tripPlanId}`,
...planUrls(tripPlanId),
...reuseEnvelopeFields(reuse),
}, options, searchResults, opts.full, "--sort"), null, 2) + "\n");

@@ -555,3 +687,3 @@ return;

if (opts.agent) {
const planUrl = `${deriveBaseUrl(getApiUrl())}/plans/${tripPlanId}`;
const planUrl = clientPlanUrl(tripPlanId);
const lines = [];

@@ -561,2 +693,4 @@ lines.push(`### Hotels (${opts.location})`);

lines.push(`_No plan given — created draft plan \`${tripPlanId}\`._`);
for (const w of reuse.warnings)
lines.push(`> ⚠ ${w}`);
if (options.length === 0) {

@@ -581,7 +715,8 @@ lines.push("_No options yet — the search is still fetching inventory._");

}
writeReuseWarnings(reuse.warnings);
if (options.length === 0) {
const loc = opts.location;
process.stderr.write(chalk.dim(`No options yet — the search may still be fetching inventory.\n`));
process.stderr.write(chalk.dim(` Poll: voyagier selection-options ${shellArg(selectionId)} --wait\n\n`));
process.stderr.write(chalk.yellow(`If it stays empty, no hotels matched "${loc}" on these dates.\n\n`));
writePollHint(selectionId);
process.stderr.write(chalk.yellow(`\nIf it stays empty, no hotels matched "${loc}" on these dates.\n\n`));
process.stderr.write(chalk.dim("Suggestions:\n"));

@@ -598,3 +733,3 @@ if (looksLikeAirportCode(loc)) {

process.stderr.write(chalk.dim(` • Check the web UI for expanded search options:\n`));
process.stderr.write(chalk.dim(` ${deriveBaseUrl(getApiUrl())}/plans/${tripPlanId}\n`));
process.stderr.write(chalk.dim(` ${clientPlanUrl(tripPlanId)}\n`));
return;

@@ -727,3 +862,3 @@ }

selectionId: selectionId,
url: `${deriveBaseUrl(getApiUrl())}/plans/${tripPlanId}`,
...planUrls(tripPlanId),
}, options, searchResults, opts.full, "--sort"), null, 2) + "\n");

@@ -733,3 +868,3 @@ return;

if (opts.agent) {
const planUrl = `${deriveBaseUrl(getApiUrl())}/plans/${tripPlanId}`;
const planUrl = clientPlanUrl(tripPlanId);
const lines = [];

@@ -736,0 +871,0 @@ lines.push(`### Activities (${opts.destination})`);

@@ -8,2 +8,3 @@ import { printPlanFooter } from "../plan-footer.js";

import { deriveBaseUrl, shellArg } from "../utils.js";
import { clientPlanUrl, planUrls } from "../plan-urls.js";
import { GET_PLAN_STATUS } from "../queries.js";

@@ -325,3 +326,3 @@ import { resolveHotelCodes, buildPlanStatus } from "./plan-status.js";

...(roomStep ? { roomSelectionId: roomStep } : {}),
url: `${deriveBaseUrl(getApiUrl())}/plans/${state.tripPlanId}`,
...planUrls(state.tripPlanId),
...(waitOutcome !== undefined ? waitJsonFragment(waitOutcome) : {}),

@@ -331,3 +332,3 @@ }, state.tripPlanId);

else if (opts.agent) {
const planUrl = `${deriveBaseUrl(getApiUrl())}/plans/${state.tripPlanId}`;
const planUrl = clientPlanUrl(state.tripPlanId);
const icon = state.type === "flights" ? "✈️" : state.type === "activities" ? "🎯" : "🏨";

@@ -334,0 +335,0 @@ const nextSteps = [

import chalk from "chalk";
import { graphql } from "../api.js";
import { GET_SELECTION_WITH_MONITOR, GET_BLUEPRINT_MONITOR, REFRESH_SELECTION_OPTIONS, GET_HOTEL_OPTION_DATA, } from "../queries.js";
import { GET_HOTEL_OPTION_DATA } from "../queries.js";
import { deriveRoomStay } from "../hotel-format.js";
import { jsonOutput } from "../output.js";
import { CliError, CliErrorCode } from "../errors.js";
import { classifySelection, isTerminal, } from "../selection-status.js";
import { isTerminal } from "../selection-status.js";
import { deriveChosen, deriveBlockedOn } from "../choices.js";
import { loadSelectionState, pollSelectionOptions, DEFAULT_RETRY_AFTER_MS, } from "../selection-wait.js";
import { spinnerAnimates, startSpinner } from "../spinner.js";
export { deriveChosen, deriveBlockedOn };
async function loadSelectionState(selectionId, retryAfterMs) {
const data = await graphql(GET_SELECTION_WITH_MONITOR, { tripPlanSelectionId: selectionId });
const raw = data.getTripPlanSelection;
if (!raw || !raw.id) {
throw new CliError(CliErrorCode.NOT_FOUND, `Selection ${selectionId} not found.`);
}
let monitor = null;
if (raw.blueprintMonitorId) {
try {
const m = await graphql(GET_BLUEPRINT_MONITOR, {
id: raw.blueprintMonitorId,
});
monitor = m.blueprintMonitor;
}
catch {
monitor = null;
}
}
const state = {
id: raw.id,
type: raw.type,
blueprintMonitorId: raw.blueprintMonitorId,
optionCount: (raw.options ?? []).length,
monitor: monitor
? {
id: monitor.id,
fetchedAt: monitor.fetchedAt,
lastFetchAttempt: monitor.lastFetchAttempt,
lastFetchError: monitor.lastFetchError,
}
: null,
};
return { raw, result: classifySelection(state, { retryAfterMs }) };
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function loadRoomStays(selectionId) {

@@ -69,3 +35,3 @@ const stays = new Map();

.action(async (selectionId, opts) => {
const retryAfterMs = 2000;
const retryAfterMs = DEFAULT_RETRY_AFTER_MS;
const parsedTimeout = parseInt(opts.timeout, 10);

@@ -77,27 +43,15 @@ const timeoutMs = Math.max(1, Number.isNaN(parsedTimeout) ? 30 : parsedTimeout) * 1000;

if (opts.wait && !isTerminal(result.status)) {
try {
await graphql(REFRESH_SELECTION_OPTIONS, { selectionId });
}
catch {
}
const startedAt = Date.now();
const deadline = startedAt + timeoutMs;
let delay = retryAfterMs;
let attempt = 0;
const spinner = spinnerAnimates() ? startSpinner("Fetching options...") : null;
try {
while (!isTerminal(result.status) && Date.now() < deadline) {
const remaining = deadline - Date.now();
await sleep(Math.min(delay, Math.max(0, remaining)));
({ raw, result } = await loadSelectionState(selectionId, retryAfterMs));
attempt++;
const elapsed = Math.round((Date.now() - startedAt) / 1000);
if (spinner) {
spinner.update(`Fetching options... (attempt ${attempt}, status=${result.status}, options=${result.optionCount}, ${elapsed}s)`);
}
else {
process.stderr.write(chalk.dim(` polling… status=${result.status} options=${result.optionCount} elapsed=${elapsed}s\n`));
}
delay = Math.min(delay * 1.5, 8000);
}
({ raw, result } = await pollSelectionOptions(selectionId, { raw, result }, { timeoutMs, retryAfterMs }, {
heartbeat: ({ attempt, status, optionCount, elapsedMs }) => {
const elapsed = Math.round(elapsedMs / 1000);
if (spinner) {
spinner.update(`Fetching options... (attempt ${attempt}, status=${status}, options=${optionCount}, ${elapsed}s)`);
}
else {
process.stderr.write(chalk.dim(` polling… status=${status} options=${optionCount} elapsed=${elapsed}s\n`));
}
},
}));
}

@@ -107,5 +61,2 @@ finally {

}
if (!isTerminal(result.status)) {
result = { ...result, status: "FETCHING", retryAfterMs };
}
}

@@ -112,0 +63,0 @@ const sortedOptions = [...(raw.options ?? [])].sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));

import chalk from "chalk";
import { graphql } from "../api.js";
import { jsonOutput } from "../output.js";
import { deriveBaseUrl, shellArg } from "../utils.js";
import { getApiUrl } from "../config.js";
import { shellArg } from "../utils.js";
import { planUrls } from "../plan-urls.js";
import { GET_TRAVELLER_CHOICES } from "../queries.js";

@@ -94,3 +94,2 @@ function formatChoiceTraveller(t) {

if (opts.json) {
const planUrl = `${deriveBaseUrl(getApiUrl())}/plans/${opts.plan}`;
jsonOutput({

@@ -133,3 +132,3 @@ ok: true,

title: result.title,
url: planUrl,
...planUrls(opts.plan),
travellerCount: allTravellerIds.length,

@@ -136,0 +135,0 @@ },

@@ -6,4 +6,4 @@ import chalk from "chalk";

import { LIST_TRIP_PLAN_TRAVELLER_GROUPS, GET_TRIP_PLAN_TRAVELLER_GROUP, CREATE_TRIP_PLAN_TRAVELLER_GROUP, UPDATE_TRIP_PLAN_TRAVELLER_GROUP, DELETE_TRIP_PLAN_TRAVELLER_GROUP, ADD_TRAVELLERS_TO_GROUP, REMOVE_TRAVELLERS_FROM_GROUP, } from "../queries.js";
import { deriveBaseUrl, shellArg } from "../utils.js";
import { getApiUrl } from "../config.js";
import { shellArg } from "../utils.js";
import { planUrls } from "../plan-urls.js";
export function buildGroupPlanContext(plan) {

@@ -13,3 +13,3 @@ return {

title: plan.title,
url: `${deriveBaseUrl(getApiUrl())}/plans/${plan.id}`,
...planUrls(plan.id),
travellerCount: plan.travellers?.length ?? null,

@@ -16,0 +16,0 @@ };

@@ -7,3 +7,4 @@ import { printPlanFooter } from "../plan-footer.js";

import { getApiUrl, getUserContext } from "../config.js";
import { validateDate, deriveBaseUrl, shellArg } from "../utils.js";
import { validateDate, deriveBaseUrl, shellArg, maskLoyaltyValue } from "../utils.js";
import { clientPlanUrl, planUrls } from "../plan-urls.js";
import { jsonOutput, fatal, warn } from "../output.js";

@@ -18,5 +19,2 @@ import { CliError, CliErrorCode } from "../errors.js";

}
function maskLoyaltyValue(value) {
return value.length > 4 ? `••••${value.slice(-4)}` : "••••";
}
function requirePassportNumberWithMetadata(opts) {

@@ -108,2 +106,3 @@ if (opts.passportNumber)

let phone = opts.phone;
let selfHotelLoyalty;
if (opts.self) {

@@ -127,2 +126,9 @@ const ctx = getUserContext();

filled.push("gender");
if (ctx.hotelLoyaltyPrograms?.length && opts.hotelLoyalty.length === 0) {
selfHotelLoyalty = ctx.hotelLoyaltyPrograms.map((p) => ({
chainCode: p.chainCode,
membershipNumber: p.membershipNumber,
}));
filled.push(`hotel loyalty (${selfHotelLoyalty.map((p) => p.chainCode).join(", ")})`);
}
if (!opts.json && filled.length > 0) {

@@ -214,8 +220,9 @@ console.log(chalk.dim(` Auto-filled from profile: ${filled.join(", ")}`));

input.hotelLoyaltyPrograms = toHotelLoyaltyInput(opts.hotelLoyalty);
else if (selfHotelLoyalty)
input.hotelLoyaltyPrograms = selfHotelLoyalty;
const data = await graphql(CREATE_TRAVELLER, { tripPlanId: opts.plan, input });
const t = data.createTripPlanTraveller;
const baseUrl = deriveBaseUrl(getApiUrl());
const planUrl = `${baseUrl}/plans/${opts.plan}`;
if (opts.json) {
process.stdout.write(JSON.stringify({ ...t, url: planUrl }, null, 2) + "\n");
process.stdout.write(JSON.stringify({ ...t, ...planUrls(opts.plan, baseUrl) }, null, 2) + "\n");
return;

@@ -255,5 +262,5 @@ }

const baseUrl = deriveBaseUrl(getApiUrl());
const planUrl = `${baseUrl}/plans/${opts.plan}`;
const planUrl = clientPlanUrl(opts.plan, baseUrl);
if (opts.json) {
process.stdout.write(JSON.stringify({ travellers: list, url: planUrl }, null, 2) + "\n");
process.stdout.write(JSON.stringify({ travellers: list, ...planUrls(opts.plan, baseUrl) }, null, 2) + "\n");
return;

@@ -260,0 +267,0 @@ }

@@ -28,2 +28,3 @@ export var CliErrorCode;

CliErrorCode["ALREADY_BOOKED"] = "ALREADY_BOOKED";
CliErrorCode["PLAN_BLOCKED"] = "PLAN_BLOCKED";
CliErrorCode["CONFIRMATION_REQUIRED"] = "CONFIRMATION_REQUIRED";

@@ -30,0 +31,0 @@ CliErrorCode["INVALID_INPUT"] = "INVALID_INPUT";

@@ -54,5 +54,40 @@ import { z } from "zod";

}
export function buildUpdateTravellerArgs(i) {
const args = ["travellers", "update", i.traveller_id];
opt(args, "--first", i.first);
opt(args, "--last", i.last);
opt(args, "--gender", i.gender);
opt(args, "--dob", i.dob);
opt(args, "--email", i.email);
opt(args, "--phone", i.phone);
opt(args, "--type", i.type);
opt(args, "--passport-number", i.passport_number);
opt(args, "--passport-country", i.passport_country);
opt(args, "--passport-nationality", i.passport_nationality);
opt(args, "--passport-expiry", i.passport_expiry);
for (const p of i.frequent_flyer ?? [])
args.push("--frequent-flyer", p);
for (const p of i.hotel_loyalty ?? [])
args.push("--hotel-loyalty", p);
bool(args, "--clear-frequent-flyer", i.clear_frequent_flyer);
bool(args, "--clear-hotel-loyalty", i.clear_hotel_loyalty);
args.push("--json");
return args;
}
export function buildGoalAddArgs(i) {
const args = ["plans", "goal-add", i.plan_id, "--type", i.type];
opt(args, "--name", i.name);
opt(args, "--relative-day", i.relative_day);
opt(args, "--sort-order", i.sort_order);
opt(args, "--date", i.date);
opt(args, "--scope", i.scope);
opt(args, "--travellers", i.travellers);
opt(args, "--idempotency-key", i.idempotency_key);
args.push("--json");
return args;
}
export function buildSearchFlightsArgs(i) {
const args = ["search", "flights", "--plan", i.plan_id, "--from", i.from, "--to", i.to, "--date", i.date];
opt(args, "--return", i.return);
opt(args, "--sort", i.sort);
args.push("--json");

@@ -62,3 +97,6 @@ return args;

export function buildSearchHotelsArgs(i) {
return ["search", "hotels", "--plan", i.plan_id, "--location", i.location, "--checkin", i.checkin, "--checkout", i.checkout, "--json"];
const args = ["search", "hotels", "--plan", i.plan_id, "--location", i.location, "--checkin", i.checkin, "--checkout", i.checkout];
opt(args, "--sort", i.sort);
args.push("--json");
return args;
}

@@ -110,2 +148,3 @@ export function buildSearchActivitiesArgs(i) {

bool(args, "--rebook", i.rebook);
bool(args, "--force-checkout", i.force_checkout);
args.push("--json");

@@ -178,4 +217,45 @@ return args;

defineTool({
name: "travellers_update",
description: "Update an existing traveller's record on a plan. Use to correct names or to fill the fields checkout requires: gender and date of birth (required at flight checkout) and passport data (hard-gates international reservations). Loyalty programs: passing frequent_flyer/hotel_loyalty REPLACES the existing list; clear_frequent_flyer/clear_hotel_loyalty remove all. At least one field must be provided.",
timeoutMs: T.short,
inputSchema: {
traveller_id: z.string().describe("Traveller id to update (from add_traveller / travellers list)."),
first: z.string().optional().describe("First name."),
last: z.string().optional().describe("Last name."),
gender: z.string().optional().describe("Gender: M | F | X (or Male | Female | Unspecified). Required at flight checkout."),
dob: z.string().optional().describe("Date of birth (YYYY-MM-DD). Required at flight checkout."),
email: z.string().optional().describe("Email address."),
phone: z.string().optional().describe("Contact phone number."),
type: z.string().optional().describe("Traveller type: Adult | Child | Infant."),
passport_number: z.string().optional().describe("Passport number (required to send any passport metadata; hard-gates international reservations)."),
passport_country: z.string().optional().describe("Passport issue country code (e.g. US). Requires passport_number."),
passport_nationality: z.string().optional().describe("Passport nationality country code (e.g. US). Requires passport_number."),
passport_expiry: z.string().optional().describe("Passport expiration (YYYY-MM). Requires passport_number."),
frequent_flyer: z.array(z.string()).optional().describe('Replace frequent-flyer programs with "AIRLINE:NUMBER", e.g. ["DL:1234567"]. Member number exactly as the airline issued it.'),
hotel_loyalty: z.array(z.string()).optional().describe('Replace hotel loyalty programs with "CHAIN:NUMBER", e.g. ["HI:12345678"]. Member number is digits only — do NOT include the chain code prefix.'),
clear_frequent_flyer: z.boolean().optional().describe("Remove all frequent-flyer programs (mutually exclusive with frequent_flyer)."),
clear_hotel_loyalty: z.boolean().optional().describe("Remove all hotel loyalty programs (mutually exclusive with hotel_loyalty)."),
},
buildArgs: (i) => buildUpdateTravellerArgs(i),
}),
defineTool({
name: "goal_add",
description: "Add a goal to a trip plan (no item/selection). A goal defines a slot the plan needs decided — e.g. an Activity goal is required before search_activities has anything to search against. type is a SelectionType (Activity, Flight, Hotel, HotelRoom, …), validated by the CLI. Returns the created goal; traveller assignment (if requested) is best-effort and surfaced in the result.",
timeoutMs: T.short,
inputSchema: {
plan_id: z.string().describe("Trip plan id."),
type: z.string().describe("SelectionType for the goal (e.g. Activity, Flight, Hotel, HotelRoom). Case-insensitive; validated against the CLI's supported types."),
name: z.string().optional().describe("Goal name. Defaults to '<type> goal' when omitted."),
relative_day: z.number().int().optional().describe("Day offset from trip start (integer)."),
sort_order: z.number().int().optional().describe("Initial sort order (integer)."),
date: z.string().optional().describe("Goal date (ISO 8601 date or datetime)."),
scope: z.string().optional().describe("Selection scope: Group | Traveller | Trip."),
travellers: z.string().optional().describe("Comma-separated traveller ids to assign after create (best-effort)."),
idempotency_key: z.string().optional().describe("Echoed in output for client-side retry tracking."),
},
buildArgs: (i) => buildGoalAddArgs(i),
}),
defineTool({
name: "search_flights",
description: "Search flights against the plan's Flight goal (REUSES the goal's selection — does not create a new one). Returns a compact envelope { selectionId, optionCount, topOptions[≤10] } (round trips also return returnSelectionId). If optionCount is 0 the async fetch is still running — poll get_selection_options with wait. Round trip: pick BOTH legs; the SAME optionId appears in both legs' lists (leg-mirrored) — picking the identical id on outbound and return is intended." +
description: "Search flights against the plan's Flight goal (REUSES the goal's selection — does not create a new one). Returns a compact envelope { selectionId, optionCount, topOptions[≤10], requestedParams } (round trips also return returnSelectionId). Because the selection is reused (not refetched), the envelope also echoes effectiveParams (the params the reused inventory was originally searched with) and a warnings[] entry starting SELECTION_REUSED_PARAMS_MISMATCH when the requested params differ — treat the results as reflecting effectiveParams in that case. If optionCount is 0 the async fetch is still running — poll get_selection_options with wait. Round trip: pick BOTH legs; the SAME optionId appears in both legs' lists (leg-mirrored) — picking the identical id on outbound and return is intended." +
INJECTION_NOTE,

@@ -189,2 +269,6 @@ timeoutMs: T.search,

return: z.string().optional().describe("Return date (YYYY-MM-DD) for a round-trip."),
sort: z
.enum(["price", "duration", "stops"])
.optional()
.describe("Optional factual single-field sort of the returned options: price (cheapest first), duration (shortest first), or stops (fewest first). Omit to preserve the server's default value ordering (index 0 is the server's value pick, NOT the cheapest)."),
},

@@ -195,3 +279,3 @@ buildArgs: (i) => buildSearchFlightsArgs(i),

name: "search_hotels",
description: "Search hotels against the plan's Hotel goal. Returns a compact envelope { selectionId, optionCount, topOptions[≤10] }. Prices are STAY TOTALS, not nightly. If optionCount is 0 the async fetch is still running — poll get_selection_options with wait, then select." +
description: "Search hotels against the plan's Hotel goal (REUSES the goal's selection). Returns a compact envelope { selectionId, optionCount, topOptions[≤10], requestedParams }. Because the selection is reused (not refetched), the envelope also echoes effectiveParams (the params the reused inventory was originally searched with) and a warnings[] entry starting SELECTION_REUSED_PARAMS_MISMATCH when the requested params differ — treat the results as reflecting effectiveParams in that case. Prices are STAY TOTALS, not nightly. If optionCount is 0 the async fetch is still running — poll get_selection_options with wait, then select." +
INJECTION_NOTE,

@@ -204,2 +288,6 @@ timeoutMs: T.search,

checkout: z.string().describe("Check-out date (YYYY-MM-DD). Ranges are INCLUSIVE of the end date."),
sort: z
.enum(["price"])
.optional()
.describe("Optional factual sort of the returned options by price (cheapest first). Omit to preserve the server's default value ordering (index 0 is the server's value pick, NOT the cheapest). Duration/stops do not apply to hotels."),
},

@@ -283,2 +371,3 @@ buildArgs: (i) => buildSearchHotelsArgs(i),

rebook: z.boolean().optional().describe("Proceed even though a Paid checkout already exists (intentional second charge)."),
force_checkout: z.boolean().optional().describe("Skip the client-side readiness guard (refuses checkout on hard traveller-data/other blockers) and trust the server's own validation."),
},

@@ -285,0 +374,0 @@ buildArgs: (i) => buildBookArgs(i),

import chalk from "chalk";
import { graphql } from "./api.js";
import { getApiUrl } from "./config.js";
import { deriveBaseUrl, formatDateRange } from "./utils.js";
import { formatDateRange } from "./utils.js";
import { clientPlanUrl, planUrls } from "./plan-urls.js";
const PLAN_FOOTER_QUERY = `query PlanFooter($id: String!) { tripPlan(id: $id) { title startDate endDate travellers { id } items { id } } }`;

@@ -12,3 +12,3 @@ export async function printPlanFooter(planId) {

return;
const url = `${deriveBaseUrl(getApiUrl())}/plans/${planId}`;
const url = clientPlanUrl(planId);
const dates = formatDateRange(p.startDate, p.endDate);

@@ -31,3 +31,3 @@ const tc = p.travellers?.length ?? 0;

title: p.title,
url: `${deriveBaseUrl(getApiUrl())}/plans/${planId}`,
...planUrls(planId),
dates: formatDateRange(p.startDate, p.endDate),

@@ -34,0 +34,0 @@ travellerCount: p.travellers?.length ?? 0,

@@ -320,2 +320,10 @@ export const GET_CART = `

`;
export const UPDATE_MY_USER = `
mutation UpdateMyUser($input: UpdateUserInput!) {
updateMyUser(input: $input) {
passport { last4 issueCountry nationalityCountry expirationDate }
frequentFlyerPrograms { airlineCode membershipNumber }
}
}
`;
export const CREATE_TRAVELLER = `

@@ -322,0 +330,0 @@ mutation CreateTraveller($tripPlanId: String!, $input: CreateTripPlanTravellerInput!) {

@@ -109,1 +109,91 @@ import { readFileSync, writeFileSync, mkdirSync, unlinkSync, existsSync, chmodSync } from "fs";

}
const SELECTION_PARAMS_FILE = join(CONFIG_DIR, "selection-params.json");
const SELECTION_PARAMS_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
const DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
const STRING_PARAM_KEYS = ["origin", "destination", "depart", "return", "checkin", "checkout"];
function sanitizeSelectionParams(raw) {
const clean = {};
for (const k of STRING_PARAM_KEYS) {
const v = raw[k];
if (v === undefined || v === null)
continue;
if (typeof v !== "string")
return null;
clean[k] = v;
}
if (raw.partySize !== undefined && raw.partySize !== null) {
if (typeof raw.partySize !== "number" || !Number.isFinite(raw.partySize))
return null;
clean.partySize = raw.partySize;
}
return clean;
}
function loadSelectionParamsMap() {
if (!existsSync(SELECTION_PARAMS_FILE))
return Object.create(null);
try {
const raw = readFileSync(SELECTION_PARAMS_FILE, "utf-8");
const parsed = JSON.parse(raw);
const map = parsed?.selections;
if (typeof map !== "object" || map === null)
return Object.create(null);
const out = Object.create(null);
for (const [id, v] of Object.entries(map)) {
if (DANGEROUS_KEYS.has(id))
continue;
if (typeof v !== "object" || v === null)
continue;
const e = v;
if (typeof e.timestamp !== "string" || typeof e.params !== "object" || e.params === null)
continue;
const params = sanitizeSelectionParams(e.params);
if (!params)
continue;
out[id] = { params, timestamp: e.timestamp };
}
return out;
}
catch (err) {
if (err instanceof SyntaxError) {
try {
unlinkSync(SELECTION_PARAMS_FILE);
}
catch { }
}
return Object.create(null);
}
}
export function getSelectionSearchParams(selectionId) {
return loadSelectionParamsMap()[selectionId]?.params ?? null;
}
export function rememberSelectionSearchParams(selectionId, params, now = new Date()) {
try {
const map = loadSelectionParamsMap();
const cutoff = now.getTime() - SELECTION_PARAMS_MAX_AGE_MS;
let mutated = false;
for (const [id, e] of Object.entries(map)) {
const t = new Date(e.timestamp).getTime();
if (!Number.isFinite(t) || t < cutoff) {
delete map[id];
mutated = true;
}
}
if (!map[selectionId]) {
map[selectionId] = { params, timestamp: now.toISOString() };
mutated = true;
}
if (!mutated)
return;
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
chmodSync(CONFIG_DIR, 0o700);
writeFileSync(SELECTION_PARAMS_FILE, JSON.stringify({ selections: map }, null, 2), { mode: 0o600 });
chmodSync(SELECTION_PARAMS_FILE, 0o600);
}
catch {
}
}
export function clearSelectionSearchParams() {
if (existsSync(SELECTION_PARAMS_FILE)) {
unlinkSync(SELECTION_PARAMS_FILE);
}
}

@@ -7,2 +7,5 @@ import chalk from "chalk";

export { formatPrice, cents } from "./format.js";
export function maskLoyaltyValue(value) {
return value.length > 4 ? `••••${value.slice(-4)}` : "••••";
}
export function extractFlightToken(bookingData) {

@@ -9,0 +12,0 @@ if (!bookingData)

{
"name": "@voyagier/cli",
"version": "2.16.0",
"version": "2.17.0",
"mcpName": "com.voyagier/cli",

@@ -5,0 +5,0 @@ "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).",

@@ -192,3 +192,3 @@ # @voyagier/cli

Once your account is granted API access, mint a personal access token at [voyagier.com/me/settings/tokens](https://voyagier.com/me/settings/tokens) and you're in. Two account tiers use the CLI today:
Once your account is granted API access, mint a personal access token at [travel.voyagier.com/me/settings/tokens](https://travel.voyagier.com/me/settings/tokens) and you're in. Two account tiers use the CLI today:

@@ -195,0 +195,0 @@ - **Travel advisors** — manage a book of clients (`voyagier clients`); plans are created against a client (`--client`).