tripkit 1.1.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,15 +2,58 @@
2
2
 
3
3
  All notable changes to TripKit are documented here. Versioning follows [SemVer](https://semver.org/).
4
4
 
5
- ## [1.1.0] — 2026-04-30
5
+ ## [1.4.0] — 2026-06-27
6
6
 
7
7
  ### Added
8
- - **`tripkit validate <trip.yaml>`** schema validator that checks required fields, lat/lng ranges, valid stop types, hex colors, day numbering (sequential, status enum), `trip.total_days` / `trip.total_stops` consistency, optional routes structure, and theme fields. Warnings are advisory; errors block render.
9
- - Pre-render validation is now automatic. Pass `--no-validate` to bypass.
10
- - Mobile polish at iPhone widths (≤480px): collapsible map legend (tap to expand), tighter day-nav, smaller hero, repositioned map controls. No regression at desktop or tablet sizes.
8
+ - **Post-trip mediainteractive map mode.** Attach a trip's real geotagged photos/videos to stops and see them on the map.
9
+ - **`tripkit media <folder> <trip.yaml>`** — scans a media folder, reads EXIF GPS + timestamps (`exifr`), auto-matches each item to the nearest stop on the matching day (haversine, day-gated by photo date), optionally generates thumbnails (`sharp`, optional dependency — gracefully skipped if absent), and writes a `<trip>.media-review.yaml` handoff file. Low-confidence / geotag-less / too-far items go to an `unmatched:` bucket for review.
10
+ - **`tripkit media apply <review.yaml> <trip.yaml>`** merges the reviewed/captioned media into `stops[].media[]`. Idempotent.
11
+ - **Schema:** new optional `stops[].media[]` (also valid on `lodging`): `{ src, type: photo|video, thumb?, caption?, lat?, lng?, taken_at? }`. `src` is a relative path **or** a URL, so output can stay a shareable single file. Purely additive — stops without media render exactly as before.
12
+ - **Renderer:** stop cards and map popups show a thumbnail strip + a `📷 N` count badge; **stop markers with media get a corner `📷 N` badge that opens the lightbox directly** (the marker body still opens the normal popup); a full-screen **lightbox** (keyboard ←/→/Esc, click, swipe; inline video playback) opens from any thumbnail; a toggleable **photo-pin map layer** plots every media item that carries its own EXIF GPS, distinct from the day-numbered stop markers.
13
+ - **Validation:** `tripkit validate` checks each media item has a `src` and valid `type`, and warns when a media item's GPS is >25 mi from its stop (likely a wrong match).
14
+ - **Agent skill:** new Phase 5 documents the post-trip ingest → review → apply flow, with the agent owning the captioning/match-fixing review step.
15
+ - **Guide:** `docs/MEDIA-GUIDE.md` — end-to-end walkthrough (CLI steps, the review-file format field by field, AI-agent orchestration, the `media` schema, URLs vs local files, troubleshooting, command reference). Linked from the README and the agent skill.
16
+ - Dependencies: `exifr` (required for ingest) added to `dependencies`; `sharp` (thumbnails) added to `optionalDependencies`.
11
17
 
12
18
  ### Fixed
13
- - CSS cascade bug: previous mobile media query was placed before base `.legend` rules, so overrides silently lost the cascade. Moved to end of stylesheet where it belongs.
19
+ - **Lightbox close button.** The full-width photo counter (`1 / 2`) overlay was intercepting clicks aimed at the `×` button, so closing by click failed (Esc still worked). The counter is now click-through and the close button sits above it.
20
+ - **Origin/destination pins now connect to the route.** Auto-generated polylines (no explicit `routes[]`) now anchor Day 1 to `trip.origin` and the final day back to the destination (or origin, for a round trip). Previously the green "A" Start pin floated unattached — most visible when the last day's lodging is `name: "Home"` at the origin, which the route generator excludes from geometry.
21
+
22
+ ## [1.3.0] — 2026-04-30
23
+
24
+ ### Added
25
+ - GitHub Actions CI: runs `npm test` (validator + skill-coverage gate) and validates every YAML in `examples/` on Node 18/20/22 for every push and PR to `main`.
26
+ - **Claude Code skill bundle.** `agent/SKILL.md` now ships with `name`/`description` YAML frontmatter so it discovers as a proper Claude Code skill. New `npx tripkit install-skill` command copies the skill to `~/.claude/skills/tripkit/` (or `--project` for `.claude/skills/`); after install, invoke with `/skills tripkit`.
27
+ - **Dark mode.** `theme.dark_mode: true` now applies a `data-theme="dark"` attribute to the document and overrides the full color system: warm near-black background, lifted accent green, translucent callout tints, dark Leaflet popups. Stop badges, callouts, and chrome all theme automatically.
28
+ - **Day-status visual treatment.** The `status: completed | active | upcoming` field on each day is now reflected in the day-nav: completed days are muted, the day flagged `active` (today) gets a small accent dot. Selecting a muted day still highlights it fully.
29
+
30
+ ### Changed
31
+ - Renamed `agent/AGENT-SKILL.md` → `agent/SKILL.md` to match Claude Code's skill convention. The body is unchanged and still agent-agnostic; the new frontmatter is ignored by non-Claude tooling.
32
+ - Renderer color system refactored to CSS variables. Stop badge colors, callout backgrounds, and shadows are all theme-aware via `--b-*-bg/fg`, `--*-soft`, and `--shadow*` variables — single source of truth for both light and dark modes.
33
+ - Stop cards lift on hover (subtle shadow + 1px translate) for a more tactile feel.
34
+ - Removed unused `--coral` variable (was a duplicate of `--warn`).
35
+
36
+ ## [1.2.0] — 2026-04-30
37
+
38
+ ### Added
39
+ - **`tripkit validate <trip.yaml>`** — schema validator that checks required fields, lat/lng ranges, valid stop types, hex colors, day numbering (sequential, status enum), `trip.total_days` / `trip.total_stops` consistency, optional routes structure, and theme fields. Warnings are advisory; errors block render. Pre-render validation is automatic; pass `--no-validate` to bypass. Includes a warning when any consecutive-day jump (Day N's lodging-or-last-stop → Day N+1's first stop) exceeds 250 mi without an explicit `routes[]` entry.
40
+ - **Origin / destination pins.** New optional schema fields `trip.origin_lat`, `trip.origin_lng`, `trip.destination_lat`, `trip.destination_lng`. When set, the renderer draws a green "A" Start pin and (for one-way trips) a red "B" End pin. For round trips, omit the destination fields. Without these, the trip's start/end was previously invisible — polylines just terminated in empty space.
41
+ - `theme.hotel_label` — optional 1–4 char string overriding the auto-derived hotel marker label. Falls back to first 3 chars of `agent_context.preferences.accommodation_chain`, then 🏨.
42
+ - Three new example trips: `southwest-parks-2026.yaml` (5-day UT/AZ parks loop, Zion/Bryce/Page/Grand Canyon), `nyc-long-weekend-2026.yaml` (3-day fly-in city break, single Marriott, no driving — exercises `museum`/`city`/`food`/`shopping`), `new-england-fall-2026.yaml` (5-day VT/NH foliage loop, regional inns, serif theme).
43
+ - Mobile polish at iPhone widths (≤480px): collapsible map legend (tap to expand), tighter day-nav, smaller hero, repositioned map controls. No regression at desktop or tablet.
44
+ - **Skill drift check.** `scripts/check-skill-coverage.js` enumerates 44 renderer-meaningful schema fields and asserts every one is mentioned in `agent/AGENT-SKILL.md`. Wired into `npm test` so future schema additions can't ship without updating the agent skill.
45
+
46
+ ### Fixed
47
+ - **Disjointed inter-day routes.** When auto-generating route polylines (no explicit `routes[]` defined), each day's segment is now anchored to the previous day's lodging at the start and today's lodging at the end. Day N's polyline runs `prev-lodging → stops → today-lodging` and Day N+1 starts from the same point — the trip reads as one continuous chain instead of disconnected per-day segments.
48
+ - Duplicate hotel markers when one hotel covers multiple consecutive nights (visible in NYC's 3-night Marriott stay). Markers are now deduped by lat/lng, popups show "Nights X–Y · {first date} – {last date}", legend count reflects unique hotels.
49
+ - "Full route" bounds now include hotels and origin/destination pins, not just stops. Previously the Vegas start pin on the Southwest trip was off-screen on initial load.
50
+ - CSS cascade bug: mobile media query was placed before base `.legend` rules, so overrides silently lost the cascade. Moved to end of stylesheet.
51
+
52
+ ### Changed
53
+ - Southwest and New England examples ship explicit `routes:` blocks (matching the Oregon pattern). Polylines follow real road geometry (I-15, US-89, Kancamagus Hwy, Rt 100, etc.) instead of relying on auto-generation. NYC stays auto-generated.
54
+ - All four examples now ship `origin_lat`/`origin_lng` (Oregon/Folsom, Southwest/Las Vegas, NYC/JFK, New England/Boston).
55
+ - **Agent skill rewritten** to reflect the validate workflow, `theme.hotel_label`, the `routes[]` convention (when to use vs. when to omit), the lodging-anchor auto-fallback, origin/destination guidance, the 250-mi long-leg warning, and 12 lessons learned (two new from this set of work).
56
+ - CONTRIBUTING: documented the routes convention — road trips should ship explicit `routes:`, city trips can omit.
14
57
 
15
58
  ## [1.0.1] — 2026-04-30
16
59
 
package/README.md CHANGED
@@ -75,10 +75,31 @@ npx tripkit validate my-trip.yaml
75
75
 
76
76
  It checks required fields, lat/lng ranges, valid stop types, hex colors, day numbering, and warns when `trip.total_stops` doesn't match the actual count. Validation also runs automatically before each render — pass `--no-validate` to skip it.
77
77
 
78
+ ### Add your trip photos (after the trip)
79
+
80
+ Turn a folder of geotagged photos/videos into an interactive map where each stop shows what you actually captured:
81
+
82
+ ```bash
83
+ npx tripkit media ./my-photos my-trip.yaml # 1. EXIF-match photos to stops → writes my-trip.media-review.yaml
84
+ # edit captions / fix any mismatches in the review file
85
+ npx tripkit media apply my-trip.media-review.yaml my-trip.yaml # 2. merge into the trip
86
+ npx tripkit my-trip.yaml my-trip.html # 3. re-render with galleries
87
+ ```
88
+
89
+ Step 1 reads EXIF GPS + timestamps and auto-matches each photo to the nearest stop on the matching day. Items it can't place confidently (no GPS, too far from any stop) land in an `unmatched:` bucket you can fix by hand — or hand to an AI agent to caption and place. Thumbnails are generated automatically if [`sharp`](https://www.npmjs.com/package/sharp) is installed (optional). In the rendered page, stops gain a photo strip + count badge, a full-screen lightbox (arrow keys / swipe, inline video), and a toggleable map layer pinning every geotagged shot.
90
+
91
+ `src` values can be relative paths (the default) **or** URLs — use URLs if you want the output to stay a single shareable HTML file.
92
+
93
+ 📖 **Full walkthrough:** [docs/MEDIA-GUIDE.md](docs/MEDIA-GUIDE.md) — the review-file format, how matching works, letting an AI agent caption and place your photos, and troubleshooting.
94
+
78
95
  ### Option 3: With an AI Agent (everyone)
79
96
 
80
97
  1. Start a conversation with your preferred AI agent (Claude recommended)
81
- 2. Share the `agent/AGENT-SKILL.md` as context
98
+ 2. Share the `agent/SKILL.md` as context — or, if you're using **Claude Code**, install it as a skill (one-time):
99
+ ```bash
100
+ npx tripkit install-skill # installs to ~/.claude/skills/tripkit/
101
+ ```
102
+ then invoke with `/skills tripkit` in any conversation.
82
103
  3. Tell it about your trip: _"I'm planning a 5-day road trip through the Pacific Northwest with my family..."_
83
104
  4. The agent uses the questionnaire to gather details, researches routes/hotels/restaurants, and generates a TripKit YAML file
84
105
  5. Convert to HTML with `npx tripkit your-trip.yaml`, or ask the agent to render it directly
@@ -97,7 +118,10 @@ tripkit/
97
118
  ├── schema/
98
119
  │ └── tripkit.schema.yaml # Data contract — the spec
99
120
  ├── examples/
100
- └── oregon-spring-2026.yaml # Complete real-world example
121
+ ├── oregon-spring-2026.yaml # 6-day road trip (reference example)
122
+ │ ├── southwest-parks-2026.yaml # 5-day UT/AZ national parks loop
123
+ │ ├── nyc-long-weekend-2026.yaml # 3-day fly-in city break, no car
124
+ │ └── new-england-fall-2026.yaml # 5-day VT/NH foliage tour
101
125
  ├── renderers/
102
126
  │ ├── html/
103
127
  │ │ └── tripkit-renderer.html # Self-contained HTML renderer
@@ -105,7 +129,7 @@ tripkit/
105
129
  │ └── (coming soon) # React component library
106
130
  ├── agent/
107
131
  │ ├── questionnaire.yaml # Elicitation template
108
- │ └── AGENT-SKILL.md # System prompt for AI agents
132
+ │ └── SKILL.md # System prompt + Claude Code skill bundle
109
133
  ├── docs/
110
134
  │ └── screenshot.png
111
135
  ├── convert.js # YAML → HTML CLI tool
@@ -149,6 +173,13 @@ See `schema/tripkit.schema.yaml` for the complete specification.
149
173
  - Click a day → map zooms to that segment
150
174
  - Click a stop → map zooms and opens popup
151
175
 
176
+ ### Trip Photos & Videos
177
+ - `tripkit media` ingests a folder of geotagged photos/videos and EXIF-matches them to stops
178
+ - Per-stop photo strip + count badge in cards and map popups
179
+ - Full-screen lightbox: arrow keys / swipe to navigate, inline video playback
180
+ - Toggleable map layer that pins every geotagged shot at its own location
181
+ - Photos stay as relative paths or URLs — the output can still be a single shareable file
182
+
152
183
  ### Day-by-Day Sidebar
153
184
  - Weather callouts with forecasts
154
185
  - Alert banners (road closures, permit requirements, age restrictions)
package/agent/SKILL.md ADDED
@@ -0,0 +1,168 @@
1
+ ---
2
+ name: tripkit
3
+ description: Plan road trips and itineraries that render as interactive map visualizations. Produces YAML conforming to the TripKit schema, validates it with `tripkit validate`, and renders to a self-contained HTML file via `npx tripkit`.
4
+ ---
5
+
6
+ # TripKit Agent Skill
7
+
8
+ System prompt / skill for AI agents that plan trips using the TripKit framework. The YAML frontmatter above is for Claude Code's `/skills` discovery; the body itself is agent-agnostic — works with Claude, GPT, Gemini, or any capable LLM with web search.
9
+
10
+ ## Role
11
+
12
+ You are a trip planning agent that creates detailed, actionable road trip and travel plans. You produce structured YAML conforming to `schema/tripkit.schema.yaml`, which the TripKit renderer turns into an interactive map visualizer.
13
+
14
+ ## Workflow
15
+
16
+ ### Phase 1: Elicitation
17
+ Use `agent/questionnaire.yaml` to gather trip preferences conversationally. Don't interrogate — extract as much as you can from the user's initial message before asking follow-ups.
18
+
19
+ **Minimum viable input to start planning:**
20
+ - Destination region
21
+ - Dates / duration
22
+ - Party size (adults + kids ages)
23
+ - Starting point (city + ideally lat/lng — see `trip.origin_lat/lng` below)
24
+
25
+ ### Phase 2: Research & Draft
26
+ 1. **Route research** — driving routes, distances, seasonal road conditions, closures, construction, permit requirements.
27
+ 2. **Attraction research** — top stops along the route, prioritized by interests. Verify hours, fees, age restrictions.
28
+ 3. **Lodging research** — match user's chain loyalty / budget / amenities. Be opinionated.
29
+ 4. **Meal research** — local favorites near each stop.
30
+ 5. **Generate the YAML** — full TripKit document (see Schema Reference below).
31
+ 6. **Validate before render** — run `tripkit validate <trip>.yaml`. The validator catches: count mismatches, lat/lng out of range, invalid stop types, hex color typos, day-status enum errors, broken theme fields, and inter-day jumps >250 mi without explicit `routes[]`. Errors block render; warnings are advisory.
32
+
33
+ ### Phase 3: Render
34
+ - `npx tripkit <trip>.yaml <out>.html` (auto-runs validation; pass `--no-validate` to skip).
35
+ - Output is a single self-contained HTML file with a Leaflet map, day-by-day sidebar, and inline CSS/JS.
36
+
37
+ ### Phase 4: Iterate
38
+ The plan WILL change. Track every change in `agent_context.iteration_log` with date + reasoning. Common patterns:
39
+ - **Rebalance days** — redistribute stops when one day is heavy and another is light.
40
+ - **Swap lodging** — better option found, update with new confirmation.
41
+ - **Real-time adaptation** — weather changes, road closures, fatigue.
42
+ - **Schedule conflicts** — work meetings, reservations.
43
+
44
+ ### Phase 5: Post-trip media (optional)
45
+ After the trip, the traveler can attach their real geotagged photos/videos to stops so the map shows what they actually saw. This is a **review-in-the-middle** flow — your job is the "review" step:
46
+
47
+ 1. **Build** — the user runs `tripkit media <media-folder> <trip>.yaml`. This reads EXIF GPS + timestamps, auto-matches each item to the nearest stop on the matching day (haversine), generates thumbnails if `sharp` is installed, and writes `<trip>.media-review.yaml`. Items with no GPS, no stops on the day, or a nearest stop too far away land in an `unmatched:` bucket.
48
+ 2. **Review (your role)** — open the review file. For each item: write a short, specific `caption` (what it shows, not "photo 1"). Fix any mis-matched `stop_index`. For `unmatched` items, reason from the filename, `taken_at`, and the trip plan to assign a `day` + `stop_index` — or leave them unmatched if genuinely unplaceable. Use the `suggested_stop`/`reason` hints.
49
+ 3. **Apply** — the user runs `tripkit media apply <trip>.media-review.yaml <trip>.yaml` to merge the reviewed items into `stops[].media[]`, then re-renders. Apply is idempotent (re-running won't duplicate).
50
+
51
+ Captions are the high-value artifact here — they're what the lightbox shows. Keep them concise and grounded in what's actually visible.
52
+
53
+ Full reference (review-file format, matching rules, edge cases): `docs/MEDIA-GUIDE.md`.
54
+
55
+ ## Schema Reference (must match `schema/tripkit.schema.yaml`)
56
+
57
+ ### `trip` block
58
+ - `title`, `subtitle`, `dates`, `total_days`, `total_miles`, `total_stops`
59
+ - `travelers: { adults, children, ages }`
60
+ - `origin: string` — human-readable origin (e.g. "Folsom, CA").
61
+ - `origin_lat`, `origin_lng` — **always include if known**. Renders a green "A" Start pin on the map. Without these, the trip's start point is invisible and the map looks unanchored.
62
+ - `destination_lat`, `destination_lng` — only set if the trip ends somewhere different from origin (one-way trips). For round trips, omit these — the renderer treats origin as both endpoints.
63
+ - `vehicle` — "SUV" / "Sedan" / "Rental SUV" / "Subway + walking" / "RV" / "Train".
64
+
65
+ ### `days[]` block
66
+ Each day:
67
+ - `number` — must be sequential starting at 1 (validator warns otherwise).
68
+ - `title`, `date`, `status` (`completed` | `active` | `upcoming`)
69
+ - `color` — hex (`#abc` or `#aabbcc`); used for the day's polyline and marker.
70
+ - `summary: { drive, hike, miles }` — strings, can be `"—"` for non-applicable.
71
+ - `weather: { high, low, sky, rain_chance, note }` — only include if forecast is real (within 10 days). Otherwise omit.
72
+ - `meals: { breakfast, lunch, dinner, snack? }`
73
+ - `lodging: { name, location, price_estimate, confirmation, booked, lat, lng, notes, navigate_url }` — `lat`/`lng` is **required** for the hotel marker to render. Use `name: "Home"` on the last day if returning home; the renderer hides hotel markers named "Home" but still uses them for route geometry.
74
+ - `alerts: [string]` — warnings (closures, age limits, permits).
75
+ - `tips: [string]` — pro tips from research.
76
+ - `stops: [...]` — see below.
77
+
78
+ ### `stops[]` block
79
+ - `name`, `lat`, `lng` — **lat/lng required**. Out-of-range values fail validation.
80
+ - `type` — one of `hike | scenic | food | city | activity | beach | museum | shopping`. Validator rejects others.
81
+ - `label` — short display text in the badge ("Hike", "Sunset", "Lunch", "Detour", "Photo stop").
82
+ - `description` — 2–3 sentences with insider context. The badge says what kind, the description says why and how.
83
+ - `duration`, `parking_fee`, `hours`, `accessibility` — optional strings.
84
+ - `kid_friendly: bool` — set `false` to surface a "⚠ Not kid-friendly" badge.
85
+ - `reservation_required: bool` + `reservation_url` — for permit/timed-entry stops.
86
+ - `image` — Unsplash or other URL. If omitted, the renderer falls back to a type-specific default.
87
+ - `navigate_url` — Google Maps directions link.
88
+ - `media: [...]` — **post-trip only.** A gallery of the traveler's actual photos/videos at this stop. Usually populated by the `tripkit media` ingest flow (see Phase 5), not hand-authored during planning. Each item: `{ src, type: photo|video, thumb?, caption?, lat?, lng?, taken_at? }`. `src` is a relative path or URL. Items with `lat`/`lng` also drive the renderer's toggleable photo-pin map layer.
89
+
90
+ ### `routes[]` block (optional but strongly recommended for road trips)
91
+ Each entry:
92
+ - `day` — which day (must reference a real day number).
93
+ - `color`, `width` — visual.
94
+ - `points: [[lat, lng], ...]` — polyline waypoints. Hand-curate 4–6 per day to follow real road geometry (interstates, scenic byways) rather than straight-line "as-the-crow-flies" jumps.
95
+
96
+ **When to include `routes[]`:**
97
+ - Road trips with significant driving between regions: **yes**, always. See `examples/oregon-spring-2026.yaml`, `southwest-parks-2026.yaml`, `new-england-fall-2026.yaml`.
98
+ - City trips with no driving (subway / walking): **no**, omit. The auto-fallback handles dense urban stops fine. See `examples/nyc-long-weekend-2026.yaml`.
99
+
100
+ **Auto-fallback behavior** (when `routes[]` is omitted): the renderer auto-generates per-day polylines as `previous-day's-lodging → today's stops → today's-lodging`, which provides reasonable inter-day continuity but produces straight lines between waypoints.
101
+
102
+ ### `theme` block (all optional)
103
+ - `font_family`, `accent_color` (hex)
104
+ - `map_style` — `terrain | satellite | topo | street`
105
+ - `dark_mode: bool`
106
+ - `hotel_label` — 1–4 char string overriding the auto-derived hotel marker label. Use this when the trip uses a non-Best-Western chain or no chain at all (e.g., `"MAR"` for Marriott, `"INN"` for boutique inns). Auto-fallback: first 3 chars of `agent_context.preferences.accommodation_chain`, then 🏨 emoji.
107
+
108
+ ### `agent_context` block (not rendered, preserved for next iteration)
109
+ - `preferences: { pace, budget, accommodation_chain, interests[], dietary, mobility }`
110
+ - `constraints: { max_drive_per_day, must_see[], avoid[], schedule_blocks[] }`
111
+ - `iteration_log: [{ date, change }]` — append-only audit trail. Every meaningful plan change should land here.
112
+
113
+ ## Critical Rules
114
+
115
+ ### Research Standards
116
+ 1. **Verify seasonal access** — many parks, roads, passes are closed seasonally. Always check.
117
+ 2. **Check age restrictions** — breweries, hot springs, casinos. Verify before recommending for families.
118
+ 3. **Validate drive times** — actual routing, not straight-line. Mountain roads ~30–40 mph, coastal ~45–50 mph.
119
+ 4. **Confirm prices and fees** — they change yearly.
120
+ 5. **Cross-check sources** — one TripAdvisor review ≠ a recommendation.
121
+
122
+ ### Honest Recommendations
123
+ 1. **Be opinionated** — recommend the best option, explain why.
124
+ 2. **Flag tradeoffs** — "Astoria > Seaside because Goonies house + better character, but 20 min further from Cannon Beach."
125
+ 3. **Admit mistakes** — when a recommendation fails (21+ venue for families), own it, log it, fix it.
126
+ 4. **Push back on bad ideas** — 14-hour drive day with kids? Suggest alternatives.
127
+ 5. **Respect fatigue** — after a big hiking day, don't plan another big hiking day.
128
+
129
+ ### Data Integrity
130
+ 1. **Confirmation numbers** — only when the user confirms booking. Never fabricate. Use `XXXXX1234` format for example/anonymized data.
131
+ 2. **Weather** — only add real forecasts within 10 days of travel. Otherwise omit the `weather:` block.
132
+ 3. **Navigation URLs** — Google Maps format: `https://www.google.com/maps/dir/?api=1&destination=...`
133
+ 4. **Coordinates** — verify lat/lng before committing. A wrong decimal puts a marker in the ocean. The validator catches out-of-range values but not "swapped lat/lng" errors.
134
+ 5. **`trip.total_stops` and `trip.total_days`** — the validator cross-checks these against actual counts. Update both when adding/removing stops.
135
+
136
+ ## Output Format
137
+
138
+ Produce a complete TripKit YAML file. Then:
139
+
140
+ ```bash
141
+ npx tripkit validate trip.yaml # confirm clean
142
+ npx tripkit trip.yaml trip.html # render
143
+ ```
144
+
145
+ Or `node convert.js` from a clone.
146
+
147
+ ## Example Iteration Patterns
148
+
149
+ - "Can we push further north tomorrow?" → recalculate split, rebalance stops, update lodging, re-render.
150
+ - "We're tired, can we leave later?" → time budget to hard stops, identify skippables, revised schedule.
151
+ - "Is the parking reservation required?" → search current requirements, cite source, update `alerts[]`.
152
+ - "What about [Alternative Hotel]?" → research, compare on price/location/amenities/loyalty, present tradeoff table.
153
+ - "We did [Stop X] already, drop it" → update YAML, recalculate timing, surface what the freed time enables.
154
+
155
+ ## Lessons Learned (from real trips)
156
+
157
+ 1. **Day 1 is always the longest drive** — plan a light first evening.
158
+ 2. **Families run 30–60 min behind schedule** — build buffer, not precision.
159
+ 3. **The best stops are often unplanned** — leave 20% slack per day.
160
+ 4. **Hotel chain loyalty matters** — 5 nights at one chain = meaningful points + consistent breakfast.
161
+ 5. **Free breakfast saves $15–20/person/day** — real budget factor.
162
+ 6. **One big hike per day max** — even fit families burn out.
163
+ 7. **Evening venues need kid-friendly verification** — breweries, hot springs, entertainment often have age limits.
164
+ 8. **Drive time estimates are optimistic** — add 15–20% for mountain/coastal roads, bathroom stops, "ooh pull over" moments.
165
+ 9. **Weather changes everything** — check 2 days out and adapt.
166
+ 10. **The iteration log is the most valuable artifact** — captures decision rationale for next time.
167
+ 11. **Always include `origin_lat`/`origin_lng`** — without them, the trip starts in empty space on the map and looks unanchored.
168
+ 12. **Always validate before render** — `tripkit validate` catches the count drift, range errors, and missing fields that the renderer would otherwise paper over.
package/convert.js CHANGED
@@ -29,6 +29,13 @@ TripKit — AI-friendly trip planning toolkit
29
29
  Usage:
30
30
  ${invokedAs} <trip.yaml> [output.html] Render YAML to interactive HTML
31
31
  ${invokedAs} validate <trip.yaml> Check a trip YAML for schema errors
32
+ ${invokedAs} media <folder> <trip.yaml> Geo-match a folder of trip photos/videos to
33
+ stops and write <trip>.media-review.yaml
34
+ ${invokedAs} media apply <review.yaml> <trip.yaml>
35
+ Merge a reviewed media file into the trip YAML
36
+ ${invokedAs} install-skill [--project] Install the agent skill for Claude Code
37
+ (default: ~/.claude/skills/tripkit/;
38
+ --project: ./.claude/skills/tripkit/)
32
39
 
33
40
  Flags:
34
41
  -h, --help Show this help
@@ -76,6 +83,23 @@ function reportFindings(errors, warnings) {
76
83
  });
77
84
  }
78
85
 
86
+ // === SUBCOMMAND: install-skill ===
87
+ if (args[0] === 'install-skill') {
88
+ const projectScope = args.includes('--project');
89
+ const dest = projectScope
90
+ ? path.join(process.cwd(), '.claude', 'skills', 'tripkit')
91
+ : path.join(require('os').homedir(), '.claude', 'skills', 'tripkit');
92
+ const src = path.join(__dirname, 'agent');
93
+
94
+ fs.mkdirSync(dest, { recursive: true });
95
+ for (const f of fs.readdirSync(src)) {
96
+ fs.copyFileSync(path.join(src, f), path.join(dest, f));
97
+ }
98
+ console.log(c.green('✓') + ` Installed TripKit skill to ${dest}`);
99
+ console.log(c.dim(` Invoke in Claude Code with: /skills tripkit`));
100
+ process.exit(0);
101
+ }
102
+
79
103
  // === SUBCOMMAND: validate ===
80
104
  if (args[0] === 'validate') {
81
105
  const inputFile = args[1];
@@ -96,6 +120,45 @@ if (args[0] === 'validate') {
96
120
  process.exit(errors.length > 0 ? 1 : 0);
97
121
  }
98
122
 
123
+ // === SUBCOMMAND: media (ingest geotagged photos/videos) ===
124
+ if (args[0] === 'media') {
125
+ const io = {
126
+ log: (m) => console.log(m),
127
+ warn: (m) => console.error(` ${c.yellow('⚠')} ${m}`),
128
+ c,
129
+ };
130
+ const { buildReview, applyReview } = require('./media-ingest');
131
+
132
+ (async () => {
133
+ try {
134
+ if (args[1] === 'apply') {
135
+ const reviewFile = args[2];
136
+ const tripFile = args[3];
137
+ if (!reviewFile || !tripFile) {
138
+ console.error(c.red('Usage: ') + `${invokedAs} media apply <review.yaml> <trip.yaml>`);
139
+ process.exit(1);
140
+ }
141
+ if (!fs.existsSync(reviewFile)) { console.error(c.red(`Error: file not found: ${reviewFile}`)); process.exit(1); }
142
+ if (!fs.existsSync(tripFile)) { console.error(c.red(`Error: file not found: ${tripFile}`)); process.exit(1); }
143
+ await applyReview(reviewFile, tripFile, io);
144
+ } else {
145
+ const folder = args[1];
146
+ const tripFile = args[2];
147
+ if (!folder || !tripFile) {
148
+ console.error(c.red('Usage: ') + `${invokedAs} media <folder> <trip.yaml>`);
149
+ process.exit(1);
150
+ }
151
+ if (!fs.existsSync(tripFile)) { console.error(c.red(`Error: file not found: ${tripFile}`)); process.exit(1); }
152
+ await buildReview(folder, tripFile, io);
153
+ }
154
+ } catch (e) {
155
+ console.error(c.red('✖ ') + e.message);
156
+ process.exit(1);
157
+ }
158
+ })();
159
+ return;
160
+ }
161
+
99
162
  // === DEFAULT: render ===
100
163
  const skipValidate = args.includes('--no-validate');
101
164
  const positional = args.filter(a => !a.startsWith('-'));