itamatrix 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/DESIGN.md +247 -0
  2. package/LICENSE +21 -0
  3. package/README.md +101 -0
  4. package/dist/browser/batch.d.ts +8 -0
  5. package/dist/browser/batch.js +87 -0
  6. package/dist/browser/batch.js.map +1 -0
  7. package/dist/browser/batch.test.d.ts +1 -0
  8. package/dist/browser/batch.test.js +38 -0
  9. package/dist/browser/batch.test.js.map +1 -0
  10. package/dist/browser/forms.d.ts +26 -0
  11. package/dist/browser/forms.js +233 -0
  12. package/dist/browser/forms.js.map +1 -0
  13. package/dist/browser/session.d.ts +20 -0
  14. package/dist/browser/session.js +126 -0
  15. package/dist/browser/session.js.map +1 -0
  16. package/dist/cache.d.ts +21 -0
  17. package/dist/cache.js +71 -0
  18. package/dist/cache.js.map +1 -0
  19. package/dist/cache.test.d.ts +1 -0
  20. package/dist/cache.test.js +79 -0
  21. package/dist/cache.test.js.map +1 -0
  22. package/dist/cli.d.ts +3 -0
  23. package/dist/cli.js +154 -0
  24. package/dist/cli.js.map +1 -0
  25. package/dist/commands/calendar.d.ts +19 -0
  26. package/dist/commands/calendar.js +47 -0
  27. package/dist/commands/calendar.js.map +1 -0
  28. package/dist/commands/calendar.test.d.ts +1 -0
  29. package/dist/commands/calendar.test.js +58 -0
  30. package/dist/commands/calendar.test.js.map +1 -0
  31. package/dist/commands/multicity.d.ts +17 -0
  32. package/dist/commands/multicity.js +50 -0
  33. package/dist/commands/multicity.js.map +1 -0
  34. package/dist/commands/multicity.test.d.ts +1 -0
  35. package/dist/commands/multicity.test.js +54 -0
  36. package/dist/commands/multicity.test.js.map +1 -0
  37. package/dist/commands/search.d.ts +20 -0
  38. package/dist/commands/search.js +43 -0
  39. package/dist/commands/search.js.map +1 -0
  40. package/dist/commands/search.test.d.ts +1 -0
  41. package/dist/commands/search.test.js +124 -0
  42. package/dist/commands/search.test.js.map +1 -0
  43. package/dist/commands/shared.d.ts +44 -0
  44. package/dist/commands/shared.js +77 -0
  45. package/dist/commands/shared.js.map +1 -0
  46. package/dist/model/spec.d.ts +63 -0
  47. package/dist/model/spec.js +29 -0
  48. package/dist/model/spec.js.map +1 -0
  49. package/dist/model/spec.test.d.ts +1 -0
  50. package/dist/model/spec.test.js +55 -0
  51. package/dist/model/spec.test.js.map +1 -0
  52. package/dist/model/types.d.ts +22080 -0
  53. package/dist/model/types.js +100 -0
  54. package/dist/model/types.js.map +1 -0
  55. package/dist/model/types.test.d.ts +1 -0
  56. package/dist/model/types.test.js +35 -0
  57. package/dist/model/types.test.js.map +1 -0
  58. package/dist/render/calendar.d.ts +21 -0
  59. package/dist/render/calendar.js +89 -0
  60. package/dist/render/calendar.js.map +1 -0
  61. package/dist/render/calendar.render.test.d.ts +1 -0
  62. package/dist/render/calendar.render.test.js +66 -0
  63. package/dist/render/calendar.render.test.js.map +1 -0
  64. package/dist/render/json.d.ts +2 -0
  65. package/dist/render/json.js +4 -0
  66. package/dist/render/json.js.map +1 -0
  67. package/dist/render/normalize.d.ts +32 -0
  68. package/dist/render/normalize.js +44 -0
  69. package/dist/render/normalize.js.map +1 -0
  70. package/dist/render/render.test.d.ts +1 -0
  71. package/dist/render/render.test.js +129 -0
  72. package/dist/render/render.test.js.map +1 -0
  73. package/dist/render/table.d.ts +2 -0
  74. package/dist/render/table.js +49 -0
  75. package/dist/render/table.js.map +1 -0
  76. package/docs/ROUTING_CODES.md +137 -0
  77. package/package.json +68 -0
  78. package/skills/itamatrix/SKILL.md +173 -0
@@ -0,0 +1,100 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Zod schemas for the subset of the `/v1/search` response that P1 consumes.
4
+ *
5
+ * Derived from `fixtures/result_full.json`. The live schema is far larger; we
6
+ * parse leniently (`.passthrough()` on objects, optional facets) so unrelated
7
+ * schema drift in fields we ignore never fails a search.
8
+ */
9
+ export const PlaceSchema = z
10
+ .object({
11
+ code: z.string(),
12
+ name: z.string().optional(),
13
+ })
14
+ .passthrough();
15
+ export const CarrierSchema = z
16
+ .object({
17
+ code: z.string(),
18
+ shortName: z.string().optional(),
19
+ })
20
+ .passthrough();
21
+ export const SliceSchema = z
22
+ .object({
23
+ origin: PlaceSchema,
24
+ destination: PlaceSchema,
25
+ departure: z.string(),
26
+ arrival: z.string(),
27
+ flights: z.array(z.string()).default([]),
28
+ cabins: z.array(z.string()).default([]),
29
+ duration: z.number().optional(),
30
+ ext: z
31
+ .object({
32
+ warnings: z
33
+ .object({ types: z.array(z.string()).default([]) })
34
+ .passthrough()
35
+ .optional(),
36
+ })
37
+ .passthrough()
38
+ .optional(),
39
+ })
40
+ .passthrough();
41
+ export const ItinerarySchema = z
42
+ .object({
43
+ slices: z.array(SliceSchema),
44
+ carriers: z.array(CarrierSchema).default([]),
45
+ distance: z
46
+ .object({ units: z.string(), value: z.number() })
47
+ .passthrough()
48
+ .optional(),
49
+ })
50
+ .passthrough();
51
+ export const SolutionSchema = z
52
+ .object({
53
+ id: z.string(),
54
+ displayTotal: z.string(),
55
+ passengerCount: z.number().optional(),
56
+ itinerary: ItinerarySchema,
57
+ ext: z
58
+ .object({
59
+ price: z.string().optional(),
60
+ pricePerMile: z.string().optional(),
61
+ totalPrice: z.string().optional(),
62
+ })
63
+ .passthrough()
64
+ .optional(),
65
+ })
66
+ .passthrough();
67
+ export const SolutionListSchema = z
68
+ .object({
69
+ solutions: z.array(SolutionSchema).default([]),
70
+ minPrice: z.string().optional(),
71
+ solutionCount: z.number().optional(),
72
+ pages: z
73
+ .object({ count: z.number(), current: z.number() })
74
+ .passthrough()
75
+ .optional(),
76
+ })
77
+ .passthrough();
78
+ export const SearchResponseSchema = z
79
+ .object({
80
+ solutionList: SolutionListSchema,
81
+ solutionCount: z.number().optional(),
82
+ })
83
+ .passthrough();
84
+ /** The raw `/v1/search` body wraps the payload under `response`. */
85
+ export function parseSearchResponse(raw) {
86
+ return SearchResponseSchema.parse(unwrapResponse(raw));
87
+ }
88
+ export function parseCalendarResponse(raw) {
89
+ const root = unwrapResponse(raw);
90
+ if (!root || typeof root !== "object") {
91
+ throw new Error("Calendar response was not a JSON object");
92
+ }
93
+ return root;
94
+ }
95
+ function unwrapResponse(raw) {
96
+ return raw && typeof raw === "object" && "response" in raw
97
+ ? raw.response
98
+ : raw;
99
+ }
100
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/model/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,35 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { readFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import { parseSearchResponse } from "./types.js";
5
+ import { normalize } from "../render/normalize.js";
6
+ const fixture = JSON.parse(readFileSync(fileURLToPath(new URL("../../fixtures/result_full.json", import.meta.url)), "utf8"));
7
+ describe("parseSearchResponse", () => {
8
+ it("parses the live fixture without throwing", () => {
9
+ const resp = parseSearchResponse(fixture);
10
+ expect(resp.solutionList.solutions.length).toBe(25);
11
+ expect(resp.solutionList.solutionCount).toBe(32);
12
+ });
13
+ it("parses a payload wrapped under `response`", () => {
14
+ const resp = parseSearchResponse({ response: fixture });
15
+ expect(resp.solutionList.solutions.length).toBe(25);
16
+ });
17
+ });
18
+ describe("normalize", () => {
19
+ it("flattens solutions into an agent-friendly shape", () => {
20
+ const result = normalize(parseSearchResponse(fixture));
21
+ expect(result.count).toBe(32);
22
+ expect(result.shown).toBe(25);
23
+ expect(result.minPrice).toBe("USD440.00");
24
+ const first = result.solutions[0];
25
+ expect(first.total).toBe("USD439.81");
26
+ expect(first.slices).toHaveLength(2);
27
+ expect(first.slices[0].origin).toBe("BOS");
28
+ expect(first.slices[0].destination).toBe("LAX");
29
+ expect(first.slices[0].flights).toEqual(["UA360"]);
30
+ expect(first.slices[0].stops).toBe(0);
31
+ expect(first.slices[0].carrier).toBe("UA");
32
+ expect(first.slices[1].warnings).toContain("OVERNIGHT");
33
+ });
34
+ });
35
+ //# sourceMappingURL=types.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.test.js","sourceRoot":"","sources":["../../src/model/types.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import type { CalendarResponse } from "../model/types.js";
2
+ /** One day of the price calendar: lowest fare found for that departure date. */
3
+ export interface CalendarEntry {
4
+ date: string;
5
+ price: string;
6
+ priceValue: number;
7
+ }
8
+ export interface FlatCalendar {
9
+ entries: CalendarEntry[];
10
+ minPrice?: string;
11
+ minDate?: string;
12
+ }
13
+ /**
14
+ * The calendar response shape is unconfirmed (no fixture — DESIGN P3), so rather
15
+ * than bind to a specific schema we deep-scan the payload for objects that pair
16
+ * a date with a price and keep the lowest price per date. When a real fixture is
17
+ * captured this can be tightened to a zod schema.
18
+ */
19
+ export declare function normalizeCalendar(resp: CalendarResponse, limit?: number): FlatCalendar;
20
+ export declare function renderCalendarJson(cal: FlatCalendar): string;
21
+ export declare function renderCalendarTable(cal: FlatCalendar): string;
@@ -0,0 +1,89 @@
1
+ import Table from "cli-table3";
2
+ import chalk from "chalk";
3
+ const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}/;
4
+ const PRICE_RE = /(\d[\d,]*\.?\d*)/;
5
+ /**
6
+ * The calendar response shape is unconfirmed (no fixture — DESIGN P3), so rather
7
+ * than bind to a specific schema we deep-scan the payload for objects that pair
8
+ * a date with a price and keep the lowest price per date. When a real fixture is
9
+ * captured this can be tightened to a zod schema.
10
+ */
11
+ export function normalizeCalendar(resp, limit) {
12
+ const byDate = new Map();
13
+ for (const { date, price } of scanForFares(resp)) {
14
+ const value = parsePrice(price);
15
+ if (value == null)
16
+ continue;
17
+ const prev = byDate.get(date);
18
+ if (!prev || value < prev.priceValue) {
19
+ byDate.set(date, { date, price, priceValue: value });
20
+ }
21
+ }
22
+ const entries = capCheapest([...byDate.values()], limit).sort((a, b) => a.date.localeCompare(b.date));
23
+ const cheapest = entries.reduce((min, e) => (!min || e.priceValue < min.priceValue ? e : min), undefined);
24
+ return { entries, minPrice: cheapest?.price, minDate: cheapest?.date };
25
+ }
26
+ /** Keep the `limit` cheapest dates (the point of a price calendar); all when unset. */
27
+ function capCheapest(entries, limit) {
28
+ if (limit == null || entries.length <= limit)
29
+ return entries;
30
+ return [...entries].sort((a, b) => a.priceValue - b.priceValue).slice(0, limit);
31
+ }
32
+ /** Yields {date, price} pairs found anywhere in the response tree. */
33
+ function* scanForFares(node) {
34
+ if (Array.isArray(node)) {
35
+ for (const item of node)
36
+ yield* scanForFares(item);
37
+ return;
38
+ }
39
+ if (!node || typeof node !== "object")
40
+ return;
41
+ const obj = node;
42
+ const date = pickValue(obj, /date|departure/i, isIsoDate);
43
+ const price = pickValue(obj, /price|total|fare|amount/i, isPriceLike);
44
+ if (date && price)
45
+ yield { date: date.slice(0, 10), price };
46
+ for (const value of Object.values(obj))
47
+ yield* scanForFares(value);
48
+ }
49
+ function pickValue(obj, keyRe, valueOk) {
50
+ for (const [k, v] of Object.entries(obj)) {
51
+ if (typeof v === "string" && keyRe.test(k) && valueOk(v))
52
+ return v;
53
+ }
54
+ return undefined;
55
+ }
56
+ function isIsoDate(v) {
57
+ return ISO_DATE_RE.test(v);
58
+ }
59
+ function isPriceLike(v) {
60
+ return PRICE_RE.test(v);
61
+ }
62
+ function parsePrice(price) {
63
+ const m = price.match(PRICE_RE);
64
+ if (!m)
65
+ return null;
66
+ const value = Number(m[1].replace(/,/g, ""));
67
+ return Number.isFinite(value) ? value : null;
68
+ }
69
+ export function renderCalendarJson(cal) {
70
+ return JSON.stringify(cal, null, 2);
71
+ }
72
+ export function renderCalendarTable(cal) {
73
+ if (cal.entries.length === 0) {
74
+ return chalk.yellow("No fares found for the given date range.");
75
+ }
76
+ const table = new Table({
77
+ head: [chalk.bold("Date"), chalk.bold("Lowest fare")],
78
+ style: { head: [], border: [] },
79
+ });
80
+ for (const e of cal.entries) {
81
+ const cheapest = e.date === cal.minDate;
82
+ const price = cheapest ? chalk.green.bold(e.price) : chalk.green(e.price);
83
+ table.push([cheapest ? chalk.bold(e.date) : e.date, price]);
84
+ }
85
+ const header = chalk.dim(`${cal.entries.length} dates` +
86
+ (cal.minPrice ? ` · cheapest ${cal.minPrice} on ${cal.minDate}` : ""));
87
+ return `${header}\n${table.toString()}`;
88
+ }
89
+ //# sourceMappingURL=calendar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calendar.js","sourceRoot":"","sources":["../../src/render/calendar.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,66 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { normalizeCalendar, renderCalendarTable } from "./calendar.js";
3
+ describe("normalizeCalendar", () => {
4
+ it("deep-scans for date→price pairs regardless of nesting", () => {
5
+ const resp = {
6
+ calendarSliceList: {
7
+ days: [
8
+ { departureDate: "2026-08-01", price: "USD439.81" },
9
+ { departureDate: "2026-08-02", price: "USD512.00" },
10
+ ],
11
+ },
12
+ };
13
+ const cal = normalizeCalendar(resp);
14
+ expect(cal.entries).toHaveLength(2);
15
+ expect(cal.minPrice).toBe("USD439.81");
16
+ expect(cal.minDate).toBe("2026-08-01");
17
+ });
18
+ it("keeps the lowest price when a date appears more than once", () => {
19
+ const resp = {
20
+ a: { date: "2026-08-01", total: "USD600" },
21
+ b: { date: "2026-08-01", total: "USD420.50" },
22
+ };
23
+ const cal = normalizeCalendar(resp);
24
+ expect(cal.entries).toHaveLength(1);
25
+ expect(cal.entries[0].price).toBe("USD420.50");
26
+ });
27
+ it("sorts entries by date and trims timestamps to YYYY-MM-DD", () => {
28
+ const resp = {
29
+ x: { departureDate: "2026-08-03T00:00-04:00", fare: "USD300" },
30
+ y: { departureDate: "2026-08-01T00:00-04:00", fare: "USD200" },
31
+ };
32
+ const cal = normalizeCalendar(resp);
33
+ expect(cal.entries.map((e) => e.date)).toEqual(["2026-08-01", "2026-08-03"]);
34
+ });
35
+ it("yields no entries for a payload with no date/price pairs", () => {
36
+ expect(normalizeCalendar({ foo: { bar: 1 } }).entries).toHaveLength(0);
37
+ });
38
+ it("caps to the cheapest N dates when a limit is given, still date-sorted", () => {
39
+ const resp = {
40
+ days: [
41
+ { departureDate: "2026-08-01", price: "USD500" },
42
+ { departureDate: "2026-08-02", price: "USD200" },
43
+ { departureDate: "2026-08-03", price: "USD300" },
44
+ ],
45
+ };
46
+ const cal = normalizeCalendar(resp, 2);
47
+ expect(cal.entries.map((e) => e.date)).toEqual(["2026-08-02", "2026-08-03"]);
48
+ expect(cal.minPrice).toBe("USD200");
49
+ });
50
+ });
51
+ describe("renderCalendarTable", () => {
52
+ it("renders a friendly message when there are no fares", () => {
53
+ expect(renderCalendarTable({ entries: [] })).toMatch(/No fares found/);
54
+ });
55
+ it("includes each date and the cheapest summary", () => {
56
+ const out = renderCalendarTable(normalizeCalendar({
57
+ days: [
58
+ { date: "2026-08-01", price: "USD439.81" },
59
+ { date: "2026-08-02", price: "USD512.00" },
60
+ ],
61
+ }));
62
+ expect(out).toMatch(/2026-08-01/);
63
+ expect(out).toMatch(/cheapest USD439.81/);
64
+ });
65
+ });
66
+ //# sourceMappingURL=calendar.render.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calendar.render.test.js","sourceRoot":"","sources":["../../src/render/calendar.render.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { FlatResult } from "./normalize.js";
2
+ export declare function renderJson(result: FlatResult): string;
@@ -0,0 +1,4 @@
1
+ export function renderJson(result) {
2
+ return JSON.stringify(result, null, 2);
3
+ }
4
+ //# sourceMappingURL=json.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json.js","sourceRoot":"","sources":["../../src/render/json.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,UAAU,CAAC,MAAkB;IAC3C,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;AACzC,CAAC"}
@@ -0,0 +1,32 @@
1
+ import type { SearchResponse } from "../model/types.js";
2
+ /** Flat, agent-friendly view of a single itinerary. */
3
+ export interface FlatSlice {
4
+ origin: string;
5
+ destination: string;
6
+ departure: string;
7
+ arrival: string;
8
+ flights: string[];
9
+ carrier?: string;
10
+ stops: number;
11
+ durationMinutes?: number;
12
+ warnings: string[];
13
+ }
14
+ export interface FlatSolution {
15
+ id: string;
16
+ total: string;
17
+ pricePerMile?: string;
18
+ carriers: string[];
19
+ slices: FlatSlice[];
20
+ }
21
+ export interface FlatResult {
22
+ count: number;
23
+ shown: number;
24
+ minPrice?: string;
25
+ solutions: FlatSolution[];
26
+ }
27
+ /**
28
+ * `limit` caps how many solutions are returned. Matrix paginates server-side
29
+ * (default 25); we can't reliably drive its page-size control, so we slice the
30
+ * returned page here to honor `--limit` consistently across commands.
31
+ */
32
+ export declare function normalize(resp: SearchResponse, limit?: number): FlatResult;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * IATA carrier codes are two characters and may contain a digit (e.g. JetBlue
3
+ * `B6`), so take the leading two-char code rather than stripping all digits.
4
+ */
5
+ function parseCarrier(flight) {
6
+ return flight?.match(/^[A-Z0-9]{2}/)?.[0];
7
+ }
8
+ function flattenSolution(sol) {
9
+ const slices = sol.itinerary.slices.map((s) => ({
10
+ origin: s.origin.code,
11
+ destination: s.destination.code,
12
+ departure: s.departure,
13
+ arrival: s.arrival,
14
+ flights: s.flights,
15
+ carrier: parseCarrier(s.flights[0]),
16
+ stops: Math.max(0, s.flights.length - 1),
17
+ durationMinutes: s.duration,
18
+ warnings: s.ext?.warnings?.types ?? [],
19
+ }));
20
+ return {
21
+ id: sol.id,
22
+ total: sol.displayTotal,
23
+ pricePerMile: sol.ext?.pricePerMile,
24
+ carriers: sol.itinerary.carriers.map((c) => c.code),
25
+ slices,
26
+ };
27
+ }
28
+ /**
29
+ * `limit` caps how many solutions are returned. Matrix paginates server-side
30
+ * (default 25); we can't reliably drive its page-size control, so we slice the
31
+ * returned page here to honor `--limit` consistently across commands.
32
+ */
33
+ export function normalize(resp, limit) {
34
+ const list = resp.solutionList;
35
+ const all = list.solutions.map(flattenSolution);
36
+ const solutions = limit != null ? all.slice(0, limit) : all;
37
+ return {
38
+ count: list.solutionCount ?? resp.solutionCount ?? all.length,
39
+ shown: solutions.length,
40
+ minPrice: list.minPrice,
41
+ solutions,
42
+ };
43
+ }
44
+ //# sourceMappingURL=normalize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize.js","sourceRoot":"","sources":["../../src/render/normalize.ts"],"names":[],"mappings":"AA8BA;;;GAGG;AACH,SAAS,YAAY,CAAC,MAAe;IACnC,OAAO,MAAM,EAAE,KAAK,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AAC5C,CAAC;AAED,SAAS,eAAe,CAAC,GAAa;IACpC,MAAM,MAAM,GAAgB,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC3D,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI;QACrB,WAAW,EAAE,CAAC,CAAC,WAAW,CAAC,IAAI;QAC/B,SAAS,EAAE,CAAC,CAAC,SAAS;QACtB,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QACnC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;QACxC,eAAe,EAAE,CAAC,CAAC,QAAQ;QAC3B,QAAQ,EAAE,CAAC,CAAC,GAAG,EAAE,QAAQ,EAAE,KAAK,IAAI,EAAE;KACvC,CAAC,CAAC,CAAC;IAEJ,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,KAAK,EAAE,GAAG,CAAC,YAAY;QACvB,YAAY,EAAE,GAAG,CAAC,GAAG,EAAE,YAAY;QACnC,QAAQ,EAAE,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QACnD,MAAM;KACP,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,IAAoB,EAAE,KAAc;IAC5D,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC;IAC/B,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IAChD,MAAM,SAAS,GAAG,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IAC5D,OAAO;QACL,KAAK,EAAE,IAAI,CAAC,aAAa,IAAI,IAAI,CAAC,aAAa,IAAI,GAAG,CAAC,MAAM;QAC7D,KAAK,EAAE,SAAS,CAAC,MAAM;QACvB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,SAAS;KACV,CAAC;AACJ,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,129 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { parseSearchResponse } from "../model/types.js";
3
+ import { normalize } from "./normalize.js";
4
+ import { renderJson } from "./json.js";
5
+ import { renderTable } from "./table.js";
6
+ const stripAnsi = (s) =>
7
+ // eslint-disable-next-line no-control-regex
8
+ s.replace(/\[[0-9;]*m/g, "");
9
+ /** Minimal hand-built `/v1/search`-shaped payload exercising normalize edges. */
10
+ const payload = {
11
+ solutionList: {
12
+ minPrice: "USD200.00",
13
+ solutionCount: 3,
14
+ pages: { count: 1, current: 1 },
15
+ solutions: [
16
+ {
17
+ id: "A1",
18
+ displayTotal: "USD200.00",
19
+ ext: { pricePerMile: "USD0.05" },
20
+ itinerary: {
21
+ carriers: [{ code: "DL" }],
22
+ slices: [
23
+ {
24
+ origin: { code: "JFK" },
25
+ destination: { code: "SFO" },
26
+ departure: "2026-08-10T08:00-04:00",
27
+ arrival: "2026-08-10T14:30-07:00",
28
+ flights: ["DL100", "DL200"], // 1 stop
29
+ cabins: ["COACH"],
30
+ duration: 390,
31
+ ext: { warnings: { types: ["OVERNIGHT"] } },
32
+ },
33
+ ],
34
+ },
35
+ },
36
+ {
37
+ id: "A2",
38
+ displayTotal: "USD250.00",
39
+ itinerary: {
40
+ carriers: [],
41
+ slices: [
42
+ {
43
+ origin: { code: "JFK" },
44
+ destination: { code: "SFO" },
45
+ departure: "2026-08-10T09:00-04:00",
46
+ arrival: "2026-08-10T12:30-07:00",
47
+ flights: [], // no flights → no carrier, 0 stops
48
+ cabins: [],
49
+ },
50
+ ],
51
+ },
52
+ },
53
+ ],
54
+ },
55
+ };
56
+ describe("normalize edges", () => {
57
+ const result = normalize(parseSearchResponse(payload));
58
+ it("counts stops from segment count", () => {
59
+ expect(result.solutions[0].slices[0].stops).toBe(1);
60
+ });
61
+ it("derives carrier from the first flight number", () => {
62
+ expect(result.solutions[0].slices[0].carrier).toBe("DL");
63
+ });
64
+ it("keeps the two-char code for alphanumeric carriers (e.g. B6287 → B6)", () => {
65
+ const payload = parseSearchResponse({
66
+ solutionList: {
67
+ solutions: [
68
+ {
69
+ id: "x",
70
+ displayTotal: "USD100",
71
+ itinerary: {
72
+ carriers: [{ code: "B6" }],
73
+ slices: [
74
+ {
75
+ origin: { code: "BOS" },
76
+ destination: { code: "JFK" },
77
+ departure: "2026-08-10T08:00",
78
+ arrival: "2026-08-10T09:30",
79
+ flights: ["B6287"],
80
+ },
81
+ ],
82
+ },
83
+ },
84
+ ],
85
+ solutionCount: 1,
86
+ },
87
+ });
88
+ expect(normalize(payload).solutions[0].slices[0].carrier).toBe("B6");
89
+ });
90
+ it("handles a slice with no flights", () => {
91
+ const s = result.solutions[1].slices[0];
92
+ expect(s.stops).toBe(0);
93
+ expect(s.carrier).toBeUndefined();
94
+ });
95
+ it("surfaces count and minPrice", () => {
96
+ expect(result.count).toBe(3);
97
+ expect(result.shown).toBe(2);
98
+ expect(result.minPrice).toBe("USD200.00");
99
+ });
100
+ it("caps shown solutions at limit while keeping the full count", () => {
101
+ const capped = normalize(parseSearchResponse(payload), 1);
102
+ expect(capped.solutions).toHaveLength(1);
103
+ expect(capped.shown).toBe(1);
104
+ expect(capped.count).toBe(3);
105
+ });
106
+ });
107
+ describe("renderJson", () => {
108
+ it("round-trips through JSON.parse", () => {
109
+ const result = normalize(parseSearchResponse(payload));
110
+ const parsed = JSON.parse(renderJson(result));
111
+ expect(parsed.solutions).toHaveLength(2);
112
+ expect(parsed.solutions[0].total).toBe("USD200.00");
113
+ });
114
+ });
115
+ describe("renderTable", () => {
116
+ it("renders prices, routes and warnings", () => {
117
+ const out = stripAnsi(renderTable(normalize(parseSearchResponse(payload))));
118
+ expect(out).toContain("USD200.00");
119
+ expect(out).toContain("JFK→SFO");
120
+ expect(out).toContain("1 stop");
121
+ expect(out).toContain("OVERNIGHT");
122
+ expect(out).toContain("2 of 3 results");
123
+ });
124
+ it("reports an empty result set", () => {
125
+ const empty = normalize(parseSearchResponse({ solutionList: { solutions: [] } }));
126
+ expect(stripAnsi(renderTable(empty))).toMatch(/No flights found/);
127
+ });
128
+ });
129
+ //# sourceMappingURL=render.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.test.js","sourceRoot":"","sources":["../../src/render/render.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { FlatResult } from "./normalize.js";
2
+ export declare function renderTable(result: FlatResult): string;
@@ -0,0 +1,49 @@
1
+ import Table from "cli-table3";
2
+ import chalk from "chalk";
3
+ function fmtTime(iso) {
4
+ // "2026-08-10T06:21-04:00" -> "08-10 06:21"
5
+ const m = iso.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}:\d{2})/);
6
+ if (!m)
7
+ return iso;
8
+ return `${m[2]}-${m[3]} ${m[4]}`;
9
+ }
10
+ function fmtDuration(min) {
11
+ if (min == null)
12
+ return "";
13
+ const h = Math.floor(min / 60);
14
+ const m = min % 60;
15
+ return `${h}h${String(m).padStart(2, "0")}`;
16
+ }
17
+ function summarizeSlice(sol) {
18
+ return sol.slices
19
+ .map((s) => {
20
+ const route = `${s.origin}→${s.destination}`;
21
+ const stops = s.stops === 0 ? "nonstop" : `${s.stops} stop`;
22
+ const dur = fmtDuration(s.durationMinutes);
23
+ const times = `${fmtTime(s.departure)}→${fmtTime(s.arrival)}`;
24
+ const warn = s.warnings.length ? ` ${chalk.yellow(s.warnings.join(","))}` : "";
25
+ return `${route} ${times} ${chalk.dim(`${stops} ${dur}`)} ${s.flights.join(" ")}${warn}`;
26
+ })
27
+ .join("\n");
28
+ }
29
+ export function renderTable(result) {
30
+ if (result.solutions.length === 0) {
31
+ return chalk.yellow("No flights found.");
32
+ }
33
+ const table = new Table({
34
+ head: [chalk.bold("Price"), chalk.bold("Carrier"), chalk.bold("Itinerary")],
35
+ wordWrap: true,
36
+ style: { head: [], border: [] },
37
+ });
38
+ for (const sol of result.solutions) {
39
+ table.push([
40
+ chalk.green(sol.total),
41
+ sol.carriers.join(",") || "—",
42
+ summarizeSlice(sol),
43
+ ]);
44
+ }
45
+ const header = chalk.dim(`${result.shown} of ${result.count} results` +
46
+ (result.minPrice ? ` · from ${result.minPrice}` : ""));
47
+ return `${header}\n${table.toString()}`;
48
+ }
49
+ //# sourceMappingURL=table.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"table.js","sourceRoot":"","sources":["../../src/render/table.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,YAAY,CAAC;AAC/B,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,SAAS,OAAO,CAAC,GAAW;IAC1B,4CAA4C;IAC5C,MAAM,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,wCAAwC,CAAC,CAAC;IAC9D,IAAI,CAAC,CAAC;QAAE,OAAO,GAAG,CAAC;IACnB,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACnC,CAAC;AAED,SAAS,WAAW,CAAC,GAAY;IAC/B,IAAI,GAAG,IAAI,IAAI;QAAE,OAAO,EAAE,CAAC;IAC3B,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC;IAC/B,MAAM,CAAC,GAAG,GAAG,GAAG,EAAE,CAAC;IACnB,OAAO,GAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;AAC9C,CAAC;AAED,SAAS,cAAc,CAAC,GAAiB;IACvC,OAAO,GAAG,CAAC,MAAM;SACd,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACT,MAAM,KAAK,GAAG,GAAG,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;QAC7C,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,OAAO,CAAC;QAC5D,MAAM,GAAG,GAAG,WAAW,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC;QAC3C,MAAM,KAAK,GAAG,GAAG,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;QAC9D,MAAM,IAAI,GAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/E,OAAO,GAAG,KAAK,IAAI,KAAK,IAAI,KAAK,CAAC,GAAG,CAAC,GAAG,KAAK,IAAI,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC;IAC3F,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,MAAkB;IAC5C,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAClC,OAAO,KAAK,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC;IAC3C,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC;QACtB,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QAC3E,QAAQ,EAAE,IAAI;QACd,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE;KAChC,CAAC,CAAC;IAEH,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;QACnC,KAAK,CAAC,IAAI,CAAC;YACT,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC;YACtB,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG;YAC7B,cAAc,CAAC,GAAG,CAAC;SACpB,CAAC,CAAC;IACL,CAAC;IAED,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CACtB,GAAG,MAAM,CAAC,KAAK,OAAO,MAAM,CAAC,KAAK,UAAU;QAC1C,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CACxD,CAAC;IACF,OAAO,GAAG,MAAM,KAAK,KAAK,CAAC,QAAQ,EAAE,EAAE,CAAC;AAC1C,CAAC"}