🎩 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.14.0
to
2.15.0
+164
dist/commands/scaffold.js
import chalk from "chalk";
import { graphql } from "../api.js";
import { CREATE_TRIP_PLAN, CREATE_TRAVELLER_BRIEF, LIST_TRIP_PLAN_GOALS, DELETE_TRIP_PLAN_GOAL, } from "../queries.js";
import { shellArg } from "../utils.js";
import { progress, warn } from "../output.js";
import { CliError, CliErrorCode } from "../errors.js";
import { resolveClient } from "./clients.js";
export function selectGoalsToPrune(goals, shape) {
const prune = new Map();
const warnings = [];
if (shape.oneWay) {
const returnGoals = goals.filter(g => g.type === "Flight" && /return/i.test(g.name ?? ""));
if (returnGoals.length === 0) {
warnings.push("--one-way: no return-flight goal found to prune (scaffold may have changed). Inspect `plans goals <planId>` and remove it with `plans goal-remove <goalId> --force`.");
}
for (const g of returnGoals)
prune.set(g.id, g);
}
if (shape.flightOnly) {
const hotelGoals = goals.filter(g => g.type === "Hotel");
if (hotelGoals.length === 0) {
warnings.push("--flight-only: no Hotel goal found to prune (scaffold may have changed). Inspect `plans goals <planId>`.");
}
for (const g of hotelGoals)
prune.set(g.id, g);
}
if (shape.hotelOnly) {
const flightish = goals.filter(g => g.type === "Flight" || g.type === "FlightJourney");
if (flightish.length === 0) {
warnings.push("--hotel-only: no Flight/FlightJourney goals found to prune (scaffold may have changed). Inspect `plans goals <planId>`.");
}
for (const g of flightish)
prune.set(g.id, g);
}
return { prune: [...prune.values()], warnings };
}
export function validateShapeFlags(opts) {
const anyShape = !!(opts.oneWay || opts.flightOnly || opts.hotelOnly);
if (!anyShape)
return;
if (opts.plan) {
throw new CliError(CliErrorCode.VALIDATION, "Shape flags (--one-way/--flight-only/--hotel-only) only apply when scaffolding a NEW plan. For an existing plan, prune goals directly: `voyagier plans goals <planId>` then `voyagier plans goal-remove <goalId> --force`.");
}
if (opts.oneWay && opts.return) {
throw new CliError(CliErrorCode.VALIDATION, "--one-way conflicts with --return. Drop one.");
}
if (opts.hotelOnly && opts.flightOnly) {
throw new CliError(CliErrorCode.VALIDATION, "--hotel-only conflicts with --flight-only. Pick one.");
}
if (opts.hotelOnly && opts.oneWay) {
throw new CliError(CliErrorCode.VALIDATION, "--hotel-only conflicts with --one-way (a hotel-only plan has no flights).");
}
if (opts.hotelOnly && (opts.to || opts.from || opts.depart || opts.return)) {
throw new CliError(CliErrorCode.VALIDATION, "--hotel-only conflicts with flight flags (--from/--to/--depart/--return). A hotel-only plan has no flights.");
}
if (opts.flightOnly && (opts.hotel || opts.checkin || opts.checkout)) {
throw new CliError(CliErrorCode.VALIDATION, "--flight-only conflicts with hotel flags (--hotel/--checkin/--checkout). Drop one.");
}
}
export function parseTravellers(names) {
return names.split(",")
.map(name => name.trim())
.filter(Boolean)
.map(name => {
const parts = name.split(/\s+/);
if (parts.length === 1)
return { firstName: parts[0], lastName: parts[0] };
return { firstName: parts.slice(0, -1).join(" "), lastName: parts[parts.length - 1] };
});
}
export async function addTravellers(tripPlanId, names, opts) {
const parsed = parseTravellers(names);
if (parsed.length === 0)
return [];
if (!opts?.quiet && opts?.progress !== false)
progress("Adding travellers...");
const ids = [];
for (const t of parsed) {
const tData = await graphql(CREATE_TRAVELLER_BRIEF, { tripPlanId, input: { firstName: t.firstName, lastName: t.lastName, declaredTravellerType: "Adult" } });
ids.push(tData.createTripPlanTraveller.id);
}
return ids;
}
const MONTH_ABBR = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
function formatMonthYear(dateStr, now) {
let d = null;
const m = dateStr ? /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateStr.trim()) : null;
if (m) {
const [y, mo, day] = [Number(m[1]), Number(m[2]), Number(m[3])];
const parsed = new Date(y, mo - 1, day);
if (parsed.getFullYear() === y && parsed.getMonth() === mo - 1 && parsed.getDate() === day)
d = parsed;
}
if (!d)
d = now;
return `${MONTH_ABBR[d.getMonth()]} ${d.getFullYear()}`;
}
export function generateTripTitle(args, now = new Date()) {
const destination = (args.to || args.hotel)?.trim();
const monYear = formatMonthYear(args.depart || args.checkin, now);
return `${destination || "Trip"} · ${monYear}`;
}
export async function scaffoldPlan(opts) {
const chatty = !opts.quiet;
const resolved = await resolveClient(opts.client, {
interactive: opts.interactive,
carryFlags: opts.clientHintFlags,
});
if (resolved.autoResolved && chatty) {
const note = resolved.isSelf
? `auto-resolved client: you (${resolved.name}, self)\n`
: `auto-resolved client: ${resolved.name} (${resolved.id})\n`;
process.stderr.write(chalk.dim(note));
}
if (chatty && opts.progress !== false)
progress("Creating trip plan...");
const planInput = { clientId: resolved.id, title: opts.title };
const planData = await graphql(CREATE_TRIP_PLAN, { input: planInput }, { dryRun: opts.dryRun });
const plan = planData.createTripPlan;
const travellerIds = opts.travellers
? await addTravellers(plan.id, opts.travellers, { quiet: opts.quiet, progress: opts.progress })
: [];
const prunedGoals = [];
const pruneWarnings = [];
const shape = {
oneWay: !!opts.shape?.oneWay,
flightOnly: !!opts.shape?.flightOnly,
hotelOnly: !!opts.shape?.hotelOnly,
};
if (shape.oneWay || shape.flightOnly || shape.hotelOnly) {
if (chatty && opts.progress !== false)
progress("Pruning goals to match trip shape...");
const goalsData = await graphql(LIST_TRIP_PLAN_GOALS, { tripPlanId: plan.id });
const { prune, warnings } = selectGoalsToPrune(goalsData.tripPlanGoals ?? [], shape);
pruneWarnings.push(...warnings);
for (const g of prune) {
try {
const del = await graphql(DELETE_TRIP_PLAN_GOAL, { id: g.id });
if (del.deleteTripPlanGoal === true) {
prunedGoals.push({ id: g.id, name: g.name ?? undefined, type: g.type });
}
else {
pruneWarnings.push(`Server declined to delete goal "${g.name ?? g.id}" (${g.type}). Remove it manually: voyagier plans goal-remove ${shellArg(g.id)} --force`);
}
}
catch (err) {
const message = (err instanceof Error ? err.message : String(err)).replace(/\s+/g, " ");
pruneWarnings.push(`Failed to delete goal "${g.name ?? g.id}" (${g.type}): ${message}. Remove it manually: voyagier plans goal-remove ${shellArg(g.id)} --force`);
}
}
for (const w of pruneWarnings)
warn(w);
}
return {
plan,
client: { id: resolved.id, name: resolved.name, autoResolved: resolved.autoResolved, isSelf: resolved.isSelf },
travellerIds,
prunedGoals,
pruneWarnings,
};
}
import { flushTelemetry } from "./telemetry.js";
export async function gracefulExit(code) {
process.exitCode = code;
await flushTelemetry(250);
process.exit(code);
}
import { createInterface } from "readline/promises";
import chalk from "chalk";
export function isInteractive(opts = {}) {
const noInput = opts.noInput === true || opts.input === false;
return (process.stdin.isTTY === true &&
!process.env.CI &&
!opts.json &&
!opts.agent &&
!noInput);
}
export async function promptText(question, opts = {}) {
const rl = createInterface({ input: process.stdin, output: process.stderr });
try {
const answer = (await rl.question(question)).trim();
if (!answer && opts.default !== undefined)
return opts.default;
return answer;
}
finally {
rl.close();
}
}
export async function promptPick(question, items, render, onGiveUp) {
const rl = createInterface({ input: process.stdin, output: process.stderr });
try {
const MAX_ATTEMPTS = 3;
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
process.stderr.write(question + "\n");
items.forEach((item, i) => {
process.stderr.write(` [${i + 1}] ${render(item)}\n`);
});
const raw = (await rl.question("> ")).trim();
const n = Number(raw);
if (raw !== "" && Number.isInteger(n) && n >= 1 && n <= items.length) {
return items[n - 1];
}
if (attempt < MAX_ATTEMPTS - 1) {
process.stderr.write(chalk.dim(`Please enter a number between 1 and ${items.length}.\n`));
}
}
throw onGiveUp;
}
finally {
rl.close();
}
}
+4
-2

@@ -21,3 +21,3 @@ # Voyagier CLI — Agent Reference

- **Computed itinerary.** `voyagier itinerary <planId>` reads the platform's `tripPlanEvents` resolver.
- **Advisor CRM.** `voyagier clients` manages clients; a plan requires a `clientId`.
- **Advisor CRM.** `voyagier clients` manages clients; a plan requires a `clientId`. **Exception — planning for the account owner themself** (trip-planner accounts; `whoami` shows the tier): skip client management entirely and omit `--client` — the CLI resolves the owner automatically. Don't `clients upsert` the owner's own email; that creates a redundant client record.
- **Self-check.** `voyagier doctor` verifies auth, schema reachability, state, and version.

@@ -53,3 +53,5 @@

# 1) Resolve a client (idempotent by email)
# 1) Resolve a client (idempotent by email). SKIP this step when the trip is
# for the account owner themself (trip-planner accounts) — just omit
# --client in step 2 and the CLI resolves the owner automatically.
voyagier clients upsert --email "smith@example.com" --name "Smith Family" --type Individual --json

@@ -56,0 +58,0 @@ # Returns: { client: { id, name, ... }, ok: true, created: true|false }

import { getApiUrl, getToken } from "./config.js";
import { getTraceId } from "./telemetry.js";
import { gracefulExit } from "./exit.js";
import { verbose } from "./verbose.js";

@@ -22,3 +23,3 @@ import { CliError, CliErrorCode, authFailedMessage } from "./errors.js";

process.stderr.write("--- END DRY RUN ---\n\n");
process.exit(0);
await gracefulExit(0);
}

@@ -86,2 +87,24 @@ let res;

}
const legacyModeQueries = new Set();
export function __resetFieldFallbackCache() {
legacyModeQueries.clear();
}
export async function graphqlWithFieldFallback(enrichedQuery, legacyQuery, fieldPattern, variables, options) {
if (legacyModeQueries.has(enrichedQuery)) {
return await graphql(legacyQuery, variables, options);
}
try {
return await graphql(enrichedQuery, variables, options);
}
catch (err) {
const isUnknownNewField = err instanceof CliError &&
err.code === CliErrorCode.SCHEMA_DRIFT &&
fieldPattern.test(err.message);
if (isUnknownNewField) {
legacyModeQueries.add(enrichedQuery);
return await graphql(legacyQuery, variables, options);
}
throw err;
}
}
export async function streamChat(sessionId, message, callbacks) {

@@ -88,0 +111,0 @@ const apiUrl = getApiUrl();

@@ -148,4 +148,5 @@ import chalk from "chalk";

}
if (r.tripPlanItem)
console.log(` Item: ${r.tripPlanItem.title}`);
const item = r.tripPlanPlace ?? r.tripPlanProduct;
if (item)
console.log(` Item: ${item.name}`);
if (r.travellers && r.travellers.length > 0) {

@@ -152,0 +153,0 @@ const names = r.travellers.map(t => `${t.firstName} ${t.lastName}`).join(", ");

import chalk from "chalk";
import { createInterface } from "readline";
import { graphql, streamChat } from "../api.js";
import { gracefulExit } from "../exit.js";
import { CliError, CliErrorCode } from "../errors.js";

@@ -88,3 +89,3 @@ import { CREATE_CHAT_SESSION, LIST_CHAT_SESSIONS } from "../queries.js";

process.stdout.write("\n");
process.exit(0);
await gracefulExit(0);
}

@@ -141,5 +142,5 @@ catch (err) {

});
rl.on("close", () => {
rl.on("close", async () => {
console.log(chalk.dim("\nSession ended."));
process.exit(0);
await gracefulExit(0);
});

@@ -146,0 +147,0 @@ }

import chalk from "chalk";
import { graphql } from "../api.js";
import { graphql, graphqlWithFieldFallback } from "../api.js";
import { jsonOutput, fatal } from "../output.js";
import { CliError, CliErrorCode } from "../errors.js";
import { LIST_TRIP_PLAN_CLIENTS, GET_TRIP_PLAN_CLIENT, CREATE_TRIP_PLAN_CLIENT, UPDATE_TRIP_PLAN_CLIENT, } from "../queries.js";
import { promptPick } from "../prompt.js";
import { LIST_TRIP_PLAN_CLIENTS, LIST_TRIP_PLAN_CLIENTS_WITH_SELF, GET_TRIP_PLAN_CLIENT, CREATE_TRIP_PLAN_CLIENT, UPDATE_TRIP_PLAN_CLIENT, } from "../queries.js";
import { shellArg } from "../utils.js";

@@ -29,3 +30,6 @@ const VALID_TYPES = ["individual", "company", "group"];

while (page <= CLIENTS_MAX_PAGES) {
const data = await graphql(LIST_TRIP_PLAN_CLIENTS, { page, limit: CLIENTS_PAGE_SIZE });
const data = await graphqlWithFieldFallback(LIST_TRIP_PLAN_CLIENTS_WITH_SELF, LIST_TRIP_PLAN_CLIENTS, /isSelf/, {
page,
limit: CLIENTS_PAGE_SIZE,
});
const items = data.tripPlanClients.items ?? [];

@@ -48,6 +52,7 @@ out.push(...items);

const typeLabel = chalk.cyan(`[${c.clientType}]`);
const selfMarker = c.isSelf ? " " + chalk.magenta("(self)") : "";
const contact = c.email ? chalk.dim(` <${c.email}>`) : "";
return `${statusBadge} ${typeLabel} ${chalk.bold(c.name)}${contact} ${chalk.dim(c.id)}`;
return `${statusBadge} ${typeLabel} ${chalk.bold(c.name)}${selfMarker}${contact} ${chalk.dim(c.id)}`;
}
export async function resolveClient(explicit) {
export async function resolveClient(explicit, options = {}) {
if (explicit === "") {

@@ -61,3 +66,3 @@ throw new CliError(CliErrorCode.CLIENT_REQUIRED, "--client was provided but empty. Pass an id, email, name, or omit the flag to auto-resolve.");

if (!match) {
throw new CliError(CliErrorCode.NOT_FOUND, `No ACTIVE client found with email "${explicit}".\n Fix: voyagier clients list --json (then pick an id)\n Or: voyagier clients create --name "..." --type individual --email "${explicit}"`);
throw new CliError(CliErrorCode.NOT_FOUND, `No ACTIVE client found with email "${explicit}".\n Fix: voyagier clients list (then pass --client <id|name|email>)\n Or: voyagier clients create --name "..." --type individual --email "${explicit}"`);
}

@@ -73,7 +78,12 @@ return { id: match.id, name: match.name, autoResolved: false };

if (matches.length === 0) {
throw new CliError(CliErrorCode.NOT_FOUND, `No ACTIVE client found matching "${explicit}".\n Fix: voyagier clients list --json (then pick an id)`);
throw new CliError(CliErrorCode.NOT_FOUND, `No ACTIVE client found matching "${explicit}".\n Fix: voyagier clients list (then pass --client <id|name|email>)`);
}
if (matches.length > 1) {
const list = matches.map((c) => ` ${c.id} ${c.name}`).join("\n");
throw new CliError(CliErrorCode.MULTIPLE_CLIENTS, `Multiple ACTIVE clients matched "${explicit}". Specify --client <id>:\n${list}`);
const ambiguous = new CliError(CliErrorCode.MULTIPLE_CLIENTS, `Multiple ACTIVE clients matched "${explicit}". Specify --client <id|email>:\n${list}\n Tip: an email or id is unambiguous.`);
if (options.interactive) {
const chosen = await promptPick(`Multiple ACTIVE clients matched "${explicit}". Which one?`, matches, (c) => `${c.name}${c.email ? ` <${c.email}>` : ""}`, ambiguous);
return { id: chosen.id, name: chosen.name, autoResolved: false };
}
throw ambiguous;
}

@@ -87,10 +97,24 @@ return { id: matches[0].id, name: matches[0].name, autoResolved: false };

}
if (active.length > 1) {
const list = active.map((c) => ` ${c.id} ${c.name}`).join("\n");
throw new CliError(CliErrorCode.MULTIPLE_CLIENTS, `Multiple ACTIVE clients found. Specify --client <id>:\n${list}\n Fix: voyagier plan-trip --client <id>`);
if (active.length === 1) {
return { id: active[0].id, name: active[0].name, autoResolved: true, isSelf: active[0].isSelf === true };
}
return { id: active[0].id, name: active[0].name, autoResolved: true };
const selfClients = active.filter((c) => c.isSelf === true);
if (selfClients.length === 1) {
return { id: selfClients[0].id, name: selfClients[0].name, autoResolved: true, isSelf: true };
}
const list = active.map((c) => ` ${c.id} ${c.name}${c.isSelf ? " (self)" : ""}`).join("\n");
const selfHint = selfClients.length > 0
? "\n Note: more than one client is flagged as your self client — pass --client <id> explicitly."
: "";
const exampleName = shellArg(active[0].name || "Client Name");
const carry = options.carryFlags ? ` ${options.carryFlags}` : "";
const ambiguous = new CliError(CliErrorCode.MULTIPLE_CLIENTS, `Multiple ACTIVE clients found. Specify --client <id|name|email>:\n${list}${selfHint}\n Fix: voyagier plan-trip --client ${exampleName}${carry} (--client accepts an id, name, or email)`);
if (options.interactive) {
const chosen = await promptPick("Multiple ACTIVE clients found. Which one?", active, (c) => `${c.name}${c.isSelf ? " (self)" : ""}${c.email ? ` <${c.email}>` : ""}`, ambiguous);
return { id: chosen.id, name: chosen.name, autoResolved: false };
}
throw ambiguous;
}
export async function resolveClientId(explicit) {
return (await resolveClient(explicit)).id;
export async function resolveClientId(explicit, options) {
return (await resolveClient(explicit, options)).id;
}

@@ -97,0 +121,0 @@ export function registerClientsCommands(program) {

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

import { graphql, AuthError } from "../api.js";
import { gracefulExit } from "../exit.js";
import { CONFIG_DIR, credentialsExist, getApiUrl, getUserContext } from "../config.js";

@@ -373,3 +374,3 @@ import { sanitizeExternalText } from "../utils.js";

if (overall === "FAIL")
process.exit(1);
await gracefulExit(1);
return;

@@ -398,4 +399,4 @@ }

if (overall === "FAIL")
process.exit(1);
await gracefulExit(1);
});
}
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createServer } from "../mcp/server.js";
import { TOOLS } from "../mcp/tools.js";
import { gracefulExit } from "../exit.js";
export function registerMcpCommand(program) {

@@ -22,3 +23,3 @@ program

process.exitCode = 0;
setTimeout(() => process.exit(0), 2000).unref();
setTimeout(() => void gracefulExit(0), 2000).unref();
};

@@ -25,0 +26,0 @@ process.on("SIGINT", () => void shutdown());

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

import { SEARCH_PLACES, SEARCH_EXTERNAL_PLACES, GET_PLACE_BY_ID, GET_PLACE_BY_EXTERNAL_ID, GET_TRIP_PLAN_PLACES, UPSERT_TRIP_PLAN_PLACE, REMOVE_TRIP_PLAN_PLACE, HIGHLIGHT_TRIP_PLACE, UNHIGHLIGHT_TRIP_PLACE, GET_HIGHLIGHTED_TRIP_PLACES, } from "../queries.js";
function geoPointToLocation(g) {
const c = g?.coordinates;
if (!Array.isArray(c) || c.length < 2)
return null;
const [lng, lat] = c;
if (typeof lat !== "number" || typeof lng !== "number")
return null;
return { latitude: lat, longitude: lng };
}
function normalizeSearchPlace(raw) {
return {
id: raw.id,
name: raw.name ?? null,
description: raw.description ?? null,
location: geoPointToLocation(raw.location),
address: raw.address
? {
street: raw.address.streetAddress ?? null,
city: raw.address.addressLocality ?? null,
state: raw.address.addressRegion ?? null,
postalCode: raw.address.postalCode ?? null,
country: raw.address.addressCountry ?? null,
}
: null,
country: raw.country ?? null,
locality: raw.locality ?? null,
};
}
function normalizeExternalPlace(raw) {
const main = raw.geocodes?.main;
const location = main && typeof main.latitude === "number" && typeof main.longitude === "number"
? { latitude: main.latitude, longitude: main.longitude }
: null;
const loc = raw.location;
return {
id: raw.id,
name: raw.name ?? null,
description: null,
location,
address: loc
? {
street: loc.address ?? null,
city: loc.locality ?? null,
state: loc.region ?? null,
postalCode: loc.postcode ?? null,
country: loc.country ?? null,
}
: null,
country: null,
locality: null,
};
}
function normalizeTripPlanPlace(raw) {
return {
id: raw.id,
name: raw.name ?? null,
placeId: raw.placeId ?? null,
tripPlanId: raw.tripPlanId ?? null,
type: raw.type ?? null,
types: raw.types ?? null,
countryId: raw.countryId ?? null,
countryName: raw.countryName ?? null,
description: raw.description ?? null,
iataCode: raw.iataCode ?? null,
url: raw.url ?? null,
placeTimezone: raw.placeTimezone ?? null,
location: geoPointToLocation(raw.location),
};
}
function normalizeHighlightedTripPlace(raw) {
return {
id: raw.id,
ranking: raw.ranking ?? null,
category: raw.category ?? null,
detectedPlace: raw.detectedPlace
? {
id: raw.detectedPlace.id,
name: raw.detectedPlace.name ?? null,
placeId: raw.detectedPlace.placeId ?? null,
location: geoPointToLocation(raw.detectedPlace.location),
}
: null,
};
}
const VALID_HIGHLIGHT_CATEGORIES = ["attraction", "hotel", "restaurant"];

@@ -107,3 +191,3 @@ const HIGHLIGHT_CATEGORY_MAP = {

});
places = data.searchExternalPlaces ?? [];
places = (data.searchExternalPlaces ?? []).map(normalizeExternalPlace);
total = places.length;

@@ -123,3 +207,3 @@ }

});
places = data.searchPlaces?.items ?? [];
places = (data.searchPlaces?.items ?? []).map(normalizeSearchPlace);
total = data.searchPlaces?.count ?? places.length;

@@ -180,7 +264,7 @@ }

const data = await graphql(GET_PLACE_BY_EXTERNAL_ID, { externalId: id });
place = data.getPlaceByExternalId;
place = data.getPlaceByExternalId ? normalizeSearchPlace(data.getPlaceByExternalId) : null;
}
else {
const data = await graphql(GET_PLACE_BY_ID, { id });
place = data.getPlaceById;
place = data.getPlaceById ? normalizeSearchPlace(data.getPlaceById) : null;
}

@@ -259,3 +343,3 @@ if (!place) {

const data = await graphql(UPSERT_TRIP_PLAN_PLACE, { input }, { dryRun: opts.dryRun });
const place = data.upsertTripPlanPlace;
const place = normalizeTripPlanPlace(data.upsertTripPlanPlace);
if (opts.json) {

@@ -297,3 +381,3 @@ jsonOutput({

const data = await graphql(GET_HIGHLIGHTED_TRIP_PLACES, { tripId: planId, category });
const highlighted = data.highlightedTripPlaces ?? [];
const highlighted = (data.highlightedTripPlaces ?? []).map(normalizeHighlightedTripPlace);
if (opts.json) {

@@ -343,3 +427,3 @@ jsonOutput({

const data = await graphql(GET_TRIP_PLAN_PLACES, { tripPlanId: planId });
const placesList = data.getTripPlanPlaces ?? [];
const placesList = (data.getTripPlanPlaces ?? []).map(normalizeTripPlanPlace);
if (opts.json) {

@@ -346,0 +430,0 @@ jsonOutput({

import chalk from "chalk";
import { graphql } from "../api.js";
import { getApiUrl } from "../config.js";
import { GET_TRIP_PLAN_BASIC, CREATE_TRIP_PLAN_BASIC, CREATE_TRAVELLER_BRIEF, GET_TRAVELLERS_BRIEF, LIST_TRIP_PLAN_GOALS, DELETE_TRIP_PLAN_GOAL, } from "../queries.js";
import { GET_TRIP_PLAN_BASIC, GET_TRAVELLERS_BRIEF, } from "../queries.js";
import { validateDate, warnPastDate, validateIata, deriveBaseUrl, shellArg } from "../utils.js";
import { progress, warn, fatal, jsonOutput } from "../output.js";
import { CliError, CliErrorCode } from "../errors.js";
import { resolveClient } from "./clients.js";
import { scaffoldPlan, addTravellers, validateShapeFlags, selectGoalsToPrune, generateTripTitle } from "./scaffold.js";
import { isInteractive, promptText } from "../prompt.js";
export { validateShapeFlags, selectGoalsToPrune };
export function buildClientHintFlags(opts) {
const parts = [];
if (opts.title)
parts.push(`--title ${shellArg(opts.title)}`);
if (opts.from)
parts.push(`--from ${shellArg(opts.from)}`);
if (opts.to)
parts.push(`--to ${shellArg(opts.to)}`);
if (opts.depart)
parts.push(`--depart ${shellArg(opts.depart)}`);
if (opts.return)
parts.push(`--return ${shellArg(opts.return)}`);
if (opts.hotel)
parts.push(`--hotel ${shellArg(opts.hotel)}`);
if (opts.travellers)
parts.push(`--travellers ${shellArg(opts.travellers)}`);
if (opts.oneWay)
parts.push("--one-way");
if (opts.flightOnly)
parts.push("--flight-only");
if (opts.hotelOnly)
parts.push("--hotel-only");
return parts.join(" ");
}
export function parseDurationMinutes(duration) {

@@ -39,65 +65,2 @@ if (!duration)

}
export function selectGoalsToPrune(goals, shape) {
const prune = new Map();
const warnings = [];
if (shape.oneWay) {
const returnGoals = goals.filter(g => g.type === "Flight" && /return/i.test(g.name ?? ""));
if (returnGoals.length === 0) {
warnings.push("--one-way: no return-flight goal found to prune (scaffold may have changed). Inspect `plans goals <planId>` and remove it with `plans goal-remove <goalId> --force`.");
}
for (const g of returnGoals)
prune.set(g.id, g);
}
if (shape.flightOnly) {
const hotelGoals = goals.filter(g => g.type === "Hotel");
if (hotelGoals.length === 0) {
warnings.push("--flight-only: no Hotel goal found to prune (scaffold may have changed). Inspect `plans goals <planId>`.");
}
for (const g of hotelGoals)
prune.set(g.id, g);
}
if (shape.hotelOnly) {
const flightish = goals.filter(g => g.type === "Flight" || g.type === "FlightJourney");
if (flightish.length === 0) {
warnings.push("--hotel-only: no Flight/FlightJourney goals found to prune (scaffold may have changed). Inspect `plans goals <planId>`.");
}
for (const g of flightish)
prune.set(g.id, g);
}
return { prune: [...prune.values()], warnings };
}
export function validateShapeFlags(opts) {
const anyShape = !!(opts.oneWay || opts.flightOnly || opts.hotelOnly);
if (!anyShape)
return;
if (opts.plan) {
throw new CliError(CliErrorCode.VALIDATION, "Shape flags (--one-way/--flight-only/--hotel-only) only apply when scaffolding a NEW plan. For an existing plan, prune goals directly: `voyagier plans goals <planId>` then `voyagier plans goal-remove <goalId> --force`.");
}
if (opts.oneWay && opts.return) {
throw new CliError(CliErrorCode.VALIDATION, "--one-way conflicts with --return. Drop one.");
}
if (opts.hotelOnly && opts.flightOnly) {
throw new CliError(CliErrorCode.VALIDATION, "--hotel-only conflicts with --flight-only. Pick one.");
}
if (opts.hotelOnly && opts.oneWay) {
throw new CliError(CliErrorCode.VALIDATION, "--hotel-only conflicts with --one-way (a hotel-only plan has no flights).");
}
if (opts.hotelOnly && (opts.to || opts.from || opts.depart || opts.return)) {
throw new CliError(CliErrorCode.VALIDATION, "--hotel-only conflicts with flight flags (--from/--to/--depart/--return). A hotel-only plan has no flights.");
}
if (opts.flightOnly && (opts.hotel || opts.checkin || opts.checkout)) {
throw new CliError(CliErrorCode.VALIDATION, "--flight-only conflicts with hotel flags (--hotel/--checkin/--checkout). Drop one.");
}
}
function parseTravellers(names) {
return names.split(",")
.map(name => name.trim())
.filter(Boolean)
.map(name => {
const parts = name.split(/\s+/);
if (parts.length === 1)
return { firstName: parts[0], lastName: parts[0] };
return { firstName: parts.slice(0, -1).join(" "), lastName: parts[parts.length - 1] };
});
}
export function registerPlanTripCommand(program) {

@@ -149,2 +112,3 @@ program

.option("--agent", "Output plain markdown for AI agents")
.option("--no-input", "Never prompt for missing input; fail instead (for scripts, agents, CI)")
.action(async (opts) => {

@@ -155,3 +119,8 @@ const json = !!opts.json;

if (!opts.plan && !opts.title) {
fatal("--title is required when --plan is not provided.");
if (isInteractive(opts)) {
opts.title = await promptText("Trip title: ", { default: generateTripTitle(opts) });
}
if (!opts.title) {
fatal("--title is required when --plan is not provided.");
}
}

@@ -179,3 +148,11 @@ validateShapeFlags(opts);

}
const shape = {
oneWay: !!opts.oneWay,
flightOnly: !!opts.flightOnly,
hotelOnly: !!opts.hotelOnly,
};
let plan;
let travellerIds = [];
let prunedGoals = [];
let pruneWarnings = [];
if (opts.plan) {

@@ -186,25 +163,21 @@ if (!json && !agent)

plan = planData.tripPlan;
if (opts.travellers) {
travellerIds = await addTravellers(plan.id, opts.travellers, { quiet: json || agent });
}
}
else {
const resolved = await resolveClient(opts.client);
if (resolved.autoResolved) {
process.stderr.write(`auto-resolved client: ${resolved.name} (${resolved.id})\n`);
}
if (!json && !agent)
progress("Creating trip plan...");
const planInput = { clientId: resolved.id, title: opts.title };
const planData = await graphql(CREATE_TRIP_PLAN_BASIC, { input: planInput });
plan = planData.createTripPlan;
const scaffolded = await scaffoldPlan({
client: opts.client,
title: opts.title,
travellers: opts.travellers,
shape,
quiet: json || agent,
interactive: isInteractive(opts),
clientHintFlags: buildClientHintFlags(opts),
});
plan = scaffolded.plan;
travellerIds = scaffolded.travellerIds;
prunedGoals = scaffolded.prunedGoals;
pruneWarnings = scaffolded.pruneWarnings;
}
const travellers = [];
if (opts.travellers) {
if (!json && !agent)
progress("Adding travellers...");
const parsed = parseTravellers(opts.travellers);
for (const t of parsed) {
const tData = await graphql(CREATE_TRAVELLER_BRIEF, { tripPlanId: plan.id, input: { firstName: t.firstName, lastName: t.lastName, declaredTravellerType: "Adult" } });
travellers.push(tData.createTripPlanTraveller);
}
}
let travellerIds = travellers.map(t => t.id);
if (travellerIds.length === 0) {

@@ -217,33 +190,2 @@ const tData = await graphql(GET_TRAVELLERS_BRIEF, { tripPlanId: plan.id });

}
const prunedGoals = [];
const pruneWarnings = [];
const shape = {
oneWay: !!opts.oneWay,
flightOnly: !!opts.flightOnly,
hotelOnly: !!opts.hotelOnly,
};
if (shape.oneWay || shape.flightOnly || shape.hotelOnly) {
if (!json && !agent)
progress("Pruning goals to match trip shape...");
const goalsData = await graphql(LIST_TRIP_PLAN_GOALS, { tripPlanId: plan.id });
const { prune, warnings } = selectGoalsToPrune(goalsData.tripPlanGoals ?? [], shape);
pruneWarnings.push(...warnings);
for (const g of prune) {
try {
const del = await graphql(DELETE_TRIP_PLAN_GOAL, { id: g.id });
if (del.deleteTripPlanGoal === true) {
prunedGoals.push(g);
}
else {
pruneWarnings.push(`Server declined to delete goal "${g.name ?? g.id}" (${g.type}). Remove it manually: voyagier plans goal-remove ${shellArg(g.id)} --force`);
}
}
catch (err) {
const message = (err instanceof Error ? err.message : String(err)).replace(/\s+/g, " ");
pruneWarnings.push(`Failed to delete goal "${g.name ?? g.id}" (${g.type}): ${message}. Remove it manually: voyagier plans goal-remove ${shellArg(g.id)} --force`);
}
}
for (const w of pruneWarnings)
warn(w);
}
const baseUrl2 = deriveBaseUrl(getApiUrl());

@@ -250,0 +192,0 @@ const planUrl = `${baseUrl2}/plans/${plan.id}`;

@@ -8,25 +8,36 @@ import chalk from "chalk";

import { CliError, CliErrorCode } from "../../errors.js";
import { resolveClient } from "../clients.js";
import { scaffoldPlan, generateTripTitle } from "../scaffold.js";
import { isInteractive, promptText } from "../../prompt.js";
import { planUrl, typeIcon, chosenOption } from "./types.js";
import { CREATE_TRIP_PLAN, GET_TRIP_PLANS, GET_TRIP_PLAN, GET_TRIP_PLAN_SUMMARY, UPDATE_TRIP_PLAN, GET_TRIP_PLAN_WITH_DESC, DELETE_TRIP_PLAN, } from "../../queries.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";
export function registerCrudCommands(plans) {
plans
.command("create")
.description("Create a new trip plan")
.requiredOption("--title <title>", "Trip plan title")
.description("Create a new trip plan (alias of `plan-trip` — the full trip starter)")
.option("--title <title>", "Trip plan title; prompted when omitted at a TTY")
.option("--client <ref>", "Client id, email, or name. Omit to auto-resolve when exactly one ACTIVE client exists.")
.option("--json", "Output raw JSON")
.option("--dry-run", "Show the GraphQL query without executing")
.action(async (opts) => {
.option("--no-input", "Never prompt for missing input; fail instead (for scripts, agents, CI)")
.action(async (opts, command) => {
if (!opts.title) {
if (isInteractive(opts)) {
opts.title = await promptText("Trip title: ", { default: generateTripTitle({}) });
}
if (!opts.title) {
command.error("error: required option '--title <title>' not specified", {
exitCode: 1,
code: "commander.missingMandatoryOptionValue",
});
}
}
try {
const resolved = await resolveClient(opts.client);
if (resolved.autoResolved) {
process.stderr.write(chalk.dim(`auto-resolved client: ${resolved.name} (${resolved.id})\n`));
}
const input = {
clientId: resolved.id,
const { plan } = await scaffoldPlan({
client: opts.client,
title: opts.title,
};
const data = await graphql(CREATE_TRIP_PLAN, { input }, { dryRun: opts.dryRun });
const plan = data.createTripPlan;
dryRun: opts.dryRun,
interactive: isInteractive(opts),
clientHintFlags: opts.title ? `--title ${shellArg(opts.title)}` : undefined,
progress: false,
});
if (opts.json) {

@@ -45,3 +56,4 @@ const planSummary = await getPlanSummary(plan.id);

}
console.log(chalk.dim(`\n Next: voyagier travellers add --plan ${shellArg(plan.id)} --first <name> --last <name> --type ADULT`));
console.log(chalk.dim(`\n Tip: voyagier plan-trip is the full trip starter (travellers, trip shape, first searches).`));
console.log(chalk.dim(` Next: voyagier travellers add --plan ${shellArg(plan.id)} --first <name> --last <name> --type ADULT`));
await printPlanFooter(plan.id);

@@ -48,0 +60,0 @@ }

@@ -42,4 +42,7 @@ import chalk from "chalk";

const limit = parseInt(opts.limit, 10);
const data = await graphql(GET_COMMENTS, { itemId, limit });
const comments = data.tripPlanItemComments;
if (!Number.isInteger(limit) || limit < 1) {
throw new CliError(CliErrorCode.VALIDATION, "--limit must be a positive integer.");
}
const data = await graphql(GET_COMMENTS, { itemId, limit, page: 1 });
const comments = data.tripPlanItemComments.items;
if (opts.json) {

@@ -46,0 +49,0 @@ process.stdout.write(JSON.stringify({ itemId, comments }, null, 2) + "\n");

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

import { startSpinner } from "../spinner.js";
import { isInteractive, promptText } from "../prompt.js";
async function resolveDateOpt(current, opts, question, command) {
if (current)
return current;
if (isInteractive(opts)) {
const answer = await promptText(question);
if (answer)
return answer;
}
command.error("error: required option '--date <date>' not specified", {
exitCode: 1,
code: "commander.missingMandatoryOptionValue",
});
}
export async function resolveOrCreateDecisionSelection(kind, goal, tripPlanId, createMutation, createResultKey, input, quiet, progress) {

@@ -56,3 +70,3 @@ const existingId = resolveDecisionSelection(goal, kind);

}
throw new CliError(CliErrorCode.VALIDATION, '--plan <id> is required. Create one first:\n voyagier plans create --title "My Trip"');
throw new CliError(CliErrorCode.VALIDATION, '--plan <id> is required. Create one first:\n voyagier plan-trip --client <id|name|email> --title "My Trip"');
}

@@ -185,3 +199,3 @@ function parseDurationMinutes(duration) {

.requiredOption("--to <code>", "Destination airport code (e.g., NRT)")
.requiredOption("--date <date>", "Departure date (YYYY-MM-DD)")
.option("--date <date>", "Departure date (YYYY-MM-DD); prompted when omitted at a TTY")
.option("--return <date>", "Return date (YYYY-MM-DD) for round-trip")

@@ -194,3 +208,5 @@ .option("--max-stops <n>", "Maximum number of stops")

.option("--dry-run", "Show the GraphQL query without executing")
.action(async (opts) => {
.option("--no-input", "Never prompt for missing input; fail instead (for scripts, agents, CI)")
.action(async (opts, command) => {
opts.date = await resolveDateOpt(opts.date, opts, "Departure date (YYYY-MM-DD): ", command);
try {

@@ -553,3 +569,3 @@ const quiet = !!(opts.json || opts.agent);

.requiredOption("--destination <place>", "Destination name (city or region)")
.requiredOption("--date <date>", "Travel date (YYYY-MM-DD)")
.option("--date <date>", "Travel date (YYYY-MM-DD); prompted when omitted at a TTY")
.option("--query <text>", "Free text search (e.g. 'snorkeling')")

@@ -564,3 +580,5 @@ .option("--currency <code>", "Currency code", "USD")

.option("--verbose", "Show request details sent to the API")
.action(async (opts) => {
.option("--no-input", "Never prompt for missing input; fail instead (for scripts, agents, CI)")
.action(async (opts, command) => {
opts.date = await resolveDateOpt(opts.date, opts, "Travel date (YYYY-MM-DD): ", command);
try {

@@ -567,0 +585,0 @@ validateDate(opts.date, "--date");

import chalk from "chalk";
import { credentialsExist, getUserContext, getApiUrl, saveUserContext } from "../config.js";
import { graphql } from "../api.js";
import { graphqlWithFieldFallback } from "../api.js";
import { CliError, CliErrorCode, authFailedMessage } from "../errors.js";

@@ -12,2 +12,4 @@ import { deriveBaseUrl, shellArg } from "../utils.js";

};
const ME_QUERY_WITH_ROLES = `{ me { id firstName lastName email name dateOfBirth gender isAdmin isTravelAdvisor isTripPlanner canMintPats passport { last4 issueCountry nationalityCountry expirationDate } } }`;
const ME_QUERY_LEGACY = `{ me { id firstName lastName email name dateOfBirth gender passport { last4 issueCountry nationalityCountry expirationDate } } }`;
export function registerWhoamiCommand(program) {

@@ -27,3 +29,3 @@ program

try {
const data = await graphql(`{ me { id firstName lastName email name dateOfBirth gender passport { last4 issueCountry nationalityCountry expirationDate } } }`);
const data = await graphqlWithFieldFallback(ME_QUERY_WITH_ROLES, ME_QUERY_LEGACY, /isTripPlanner|canMintPats/);
me = data.me;

@@ -55,2 +57,6 @@ }

ctx.passport = me.passport ?? undefined;
ctx.isAdmin = me.isAdmin ?? undefined;
ctx.isTravelAdvisor = me.isTravelAdvisor ?? undefined;
ctx.isTripPlanner = me.isTripPlanner ?? undefined;
ctx.canMintPats = me.canMintPats ?? undefined;
}

@@ -65,2 +71,6 @@ else {

passport: me.passport ?? undefined,
isAdmin: me.isAdmin ?? undefined,
isTravelAdvisor: me.isTravelAdvisor ?? undefined,
isTripPlanner: me.isTripPlanner ?? undefined,
canMintPats: me.canMintPats ?? undefined,
homeAirports: [],

@@ -81,2 +91,20 @@ preferredCabin: "economy",

const env = baseUrl.includes("dev.") ? "dev" : baseUrl.includes("staging.") ? "staging" : "prod";
const roleFlags = {};
if (profile.isAdmin !== undefined)
roleFlags.isAdmin = profile.isAdmin;
if (profile.isTravelAdvisor !== undefined)
roleFlags.isTravelAdvisor = profile.isTravelAdvisor;
if (profile.isTripPlanner !== undefined)
roleFlags.isTripPlanner = profile.isTripPlanner;
if (profile.canMintPats !== undefined)
roleFlags.canMintPats = profile.canMintPats;
const roleLabels = [];
if (profile.isAdmin)
roleLabels.push("Admin");
if (profile.isTravelAdvisor)
roleLabels.push("Travel Advisor");
if (profile.isTripPlanner)
roleLabels.push("Trip Planner");
if (profile.canMintPats)
roleLabels.push("API Access");
if (opts.json) {

@@ -91,2 +119,3 @@ process.stdout.write(JSON.stringify({

hasPassport: !!profile.passport,
...roleFlags,
apiUrl,

@@ -105,2 +134,5 @@ environment: env,

console.log(chalk.dim(` ${infoParts.join(" · ")}`));
if (roleLabels.length > 0) {
console.log(chalk.dim(" Role: ") + chalk.cyan(roleLabels.join(" + ")));
}
const ready = [];

@@ -107,0 +139,0 @@ const missing = [];

@@ -5,2 +5,3 @@ #!/usr/bin/env node

import { trackCommand, getTraceId, isTelemetryEnabled, telemetryErrorCode } from "./telemetry.js";
import { gracefulExit } from "./exit.js";
import { credentialsExist } from "./config.js";

@@ -51,3 +52,3 @@ import { CliError } from "./errors.js";

console.log();
process.exit(0);
await gracefulExit(0);
}

@@ -72,3 +73,3 @@ try {

}
process.exit(1);
await gracefulExit(1);
}

@@ -78,4 +79,4 @@ else {

process.stderr.write(stack + "\n");
process.exit(2);
await gracefulExit(2);
}
}

@@ -161,7 +161,2 @@ export const GET_CART = `

`;
export const CREATE_TRIP_PLAN_BASIC = `
mutation CreateTripPlan($input: CreateTripPlanInput!) {
createTripPlan(input: $input) { id title startDate endDate }
}
`;
export const GET_TRIP_PLANS = `

@@ -247,7 +242,10 @@ query TripPlans($page: Int, $limit: Int) {

export const GET_COMMENTS = `
query Comments($itemId: String!, $limit: Int) {
tripPlanItemComments(itemId: $itemId, limit: $limit) {
id text parentCommentId
author { id firstName lastName }
replies { id text author { firstName lastName } }
query Comments($itemId: String!, $limit: Int!, $page: Int!) {
tripPlanItemComments(itemId: $itemId, limit: $limit, page: $page) {
count
items {
id text parentCommentId
author { id firstName lastName }
replies { id text author { firstName lastName } }
}
}

@@ -496,3 +494,4 @@ }

tripPlan { id title }
tripPlanItem { id title }
tripPlanPlace { id name }
tripPlanProduct { id name }
}

@@ -505,3 +504,4 @@ }

tripPlan { id title }
tripPlanItem { id title }
tripPlanPlace { id name }
tripPlanProduct { id name }
} }`;

@@ -519,3 +519,4 @@ export const REFRESH_BOOKING_RECORD = `

tripPlan { id title }
tripPlanItem { id title }
tripPlanPlace { id name }
tripPlanProduct { id name }
travellers { firstName lastName }

@@ -570,2 +571,27 @@ }

`;
export const LIST_TRIP_PLAN_CLIENTS_WITH_SELF = `
query TripPlanClients($page: Int!, $limit: Int!) {
tripPlanClients(page: $page, limit: $limit) {
count
page
limit
items {
id
name
email
phone
avatarUrl
description
clientType
status
isSelf
createdAt
updatedAt
}
count
page
limit
}
}
`;
export const GET_TRIP_PLAN_CLIENT = `

@@ -818,11 +844,11 @@ query TripPlanClient($id: String!) {

location {
latitude
longitude
coordinates
type
}
address {
street
city
state
streetAddress
addressLocality
addressRegion
postalCode
country
addressCountry
}

@@ -849,22 +875,15 @@ country {

name
description
geocodes {
main {
latitude
longitude
}
}
location {
latitude
longitude
}
address {
street
city
state
postalCode
address
locality
region
postcode
country
}
country {
id
name
}
locality {
id
name
}
}

@@ -880,11 +899,11 @@ }

location {
latitude
longitude
coordinates
type
}
address {
street
city
state
streetAddress
addressLocality
addressRegion
postalCode
country
addressCountry
}

@@ -909,11 +928,11 @@ country {

location {
latitude
longitude
coordinates
type
}
address {
street
city
state
streetAddress
addressLocality
addressRegion
postalCode
country
addressCountry
}

@@ -944,8 +963,7 @@ country {

iataCode
image
url
placeTimezone
location {
latitude
longitude
coordinates
type
}

@@ -966,4 +984,4 @@ }

location {
latitude
longitude
coordinates
type
}

@@ -987,8 +1005,7 @@ }

iataCode
image
url
placeTimezone
location {
latitude
longitude
coordinates
type
}

@@ -995,0 +1012,0 @@ }

@@ -52,2 +52,23 @@ import { randomUUID, randomBytes } from "crypto";

}
const pendingSends = new Set();
export function __resetPendingSends() {
pendingSends.clear();
}
export async function flushTelemetry(timeoutMs = 250) {
if (pendingSends.size === 0)
return;
const drained = Promise.allSettled([...pendingSends]).then(() => undefined);
let timer;
const capped = new Promise((resolve) => {
timer = setTimeout(resolve, timeoutMs);
timer.unref?.();
});
try {
await Promise.race([drained, capped]);
}
finally {
if (timer)
clearTimeout(timer);
}
}
export function trackCommand(event) {

@@ -74,3 +95,3 @@ if (!isTelemetryEnabled())

};
fetch("https://http-intake.logs.datadoghq.com/api/v2/logs", {
const send = fetch("https://http-intake.logs.datadoghq.com/api/v2/logs", {
method: "POST",

@@ -83,4 +104,8 @@ headers: {

signal: AbortSignal.timeout(3000),
}).catch(() => {
}).then(() => undefined, () => {
});
pendingSends.add(send);
void send.finally(() => {
pendingSends.delete(send);
});
}

@@ -87,0 +112,0 @@ let _cliVersion = null;

{
"name": "@voyagier/cli",
"version": "2.14.0",
"version": "2.15.0",
"mcpName": "com.voyagier/cli",

@@ -8,3 +8,3 @@ "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).",

"bin": {
"voyagier": "./dist/index.js"
"voyagier": "dist/index.js"
},

@@ -46,7 +46,7 @@ "scripts": {

"dependencies": {
"@modelcontextprotocol/sdk": "^1.29.0",
"chalk": "^5.6.2",
"@modelcontextprotocol/sdk": "^1.30.0",
"chalk": "^6.0.0",
"commander": "^15.0.0",
"graphql": "^17.0.2",
"zod": "^3.25.76"
"zod": "^4.4.3"
},

@@ -57,3 +57,3 @@ "devDependencies": {

"@types/jest": "^30.0.0",
"@types/node": "^26.1.1",
"@types/node": "^26.1.2",
"jest": "^30.4.2",

@@ -60,0 +60,0 @@ "ts-jest": "^29.4.12",

# @voyagier/cli
[![CI](https://github.com/Voyagier-Travel/voyagier-cli/actions/workflows/tests-and-coverage.yaml/badge.svg?branch=main)](https://github.com/Voyagier-Travel/voyagier-cli/actions/workflows/tests-and-coverage.yaml)
[![npm version](https://img.shields.io/npm/v/%40voyagier%2Fcli)](https://www.npmjs.com/package/@voyagier/cli)
[![node](https://img.shields.io/node/v/%40voyagier%2Fcli)](https://www.npmjs.com/package/@voyagier/cli)
[![MCP](https://img.shields.io/badge/MCP-stdio%20server-black)](https://github.com/Voyagier-Travel/voyagier-cli#mcp-server)
[![license](https://img.shields.io/npm/l/%40voyagier%2Fcli)](https://github.com/Voyagier-Travel/voyagier-cli/blob/main/LICENSE)

@@ -10,3 +12,3 @@ Search flights, book activities, manage trip plans — from your terminal. Everything syncs to [voyagier.com](https://voyagier.com).

```bash
npm install -g @voyagier/cli
npm install -g voyagier # or the canonical package: @voyagier/cli
voyagier auth set-token <your-token>

@@ -16,2 +18,4 @@ voyagier doctor # confirm auth + schema reachability

`voyagier` is a convenience alias that tracks the latest compatible `@voyagier/cli` release (currently `^2`). Pinning an exact version? Use the canonical package: `npm install -g @voyagier/cli@<version>`.
No install permissions (sandboxed agent, CI)? Every command works zero-install via `npx`:

@@ -188,4 +192,11 @@

Voyagier is currently invite-based. Advisors and agent builders can request access at [voyagier.com](https://voyagier.com) — once invited, you can self-serve a personal access token from your account.
Voyagier access is granted, not open signup — **request access at [voyagier.com/agents](https://voyagier.com/agents)**. That's the gate for advisors, trip-planner customers, and agent builders alike.
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:
- **Travel advisors** — manage a book of clients (`voyagier clients`); plans are created against a client (`--client`).
- **Trip planners** — paying customers planning their own travel. Just run `voyagier plan-trip` — no client setup, no `--client` flag. (`voyagier whoami` shows your tier.)
Non-admin tokens expire (90 days max, 30 by default) — mint a fresh one when yours lapses.
> **Tip:** prefer `voyagier login` (interactive prompt) over `voyagier auth set-token <token>` — it keeps your token out of shell history. For scripts, use the `VOYAGIER_TOKEN` env var.

@@ -192,0 +203,0 @@

---
name: voyagier-cli
version: 2.11.0
version: 2.14.0
description: "Book real travel from your terminal — search flights, hotels & activities, plan trips, and check out with a price-gated booking. For AI agents and travel advisors."

@@ -5,0 +5,0 @@ metadata: