From 9289ec4f8cbc64e0449f44236948c670102c5fc3 Mon Sep 17 00:00:00 2001 From: Michael Borohovski Date: Fri, 1 May 2026 14:23:56 -0700 Subject: [PATCH 1/7] feat(plugin): add Claude Code plugin manifest and marketplace Adds the .claude-plugin/ directory with two files: - plugin.json: plugin manifest with name, version, description, author, repo, license, keywords. Defines the 'travel-hacker' plugin. - marketplace.json: marketplace catalog with one entry pointing at the repo root as the plugin source. Marketplace name 'borski-travel'. Codex also reads .claude-plugin/marketplace.json (per Anthropic docs), so the same file serves both ecosystems. Validates clean via 'claude plugin validate .'. Users install via: /plugin marketplace add borski/travel-hacking-toolkit /plugin install travel-hacker@borski-travel --- .claude-plugin/marketplace.json | 32 ++++++++++++++++++++++++++++++++ .claude-plugin/plugin.json | 23 +++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 .claude-plugin/marketplace.json create mode 100644 .claude-plugin/plugin.json diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..08cb11b --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-marketplace.json", + "name": "borski-travel", + "description": "Travel hacking plugins by Borski. Flights, hotels, points, miles, awards.", + "owner": { + "name": "Michael Borohovski" + }, + "plugins": [ + { + "name": "travel-hacker", + "source": "./", + "description": "AI-powered travel hacking with points, miles, and award flights. 42 skills + 6 MCP servers for flight search, hotel comparison, loyalty balance tracking, and award optimization across 27 mileage programs.", + "author": { + "name": "Michael Borohovski" + }, + "homepage": "https://github.com/borski/travel-hacking-toolkit", + "repository": "https://github.com/borski/travel-hacking-toolkit", + "license": "MIT", + "keywords": [ + "travel", + "flights", + "hotels", + "points", + "miles", + "award-travel", + "loyalty", + "trip-planning" + ], + "category": "productivity" + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..b8f217f --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", + "name": "travel-hacker", + "version": "1.0.0", + "description": "AI-powered travel hacking with points, miles, and award flights. 42 skills + 6 MCP servers for flight search, hotel comparison, loyalty balance tracking, and award optimization across 27 mileage programs. Try: 'Plan a 10-day Scandinavia trip in August on points' or 'Find the cheapest business class to Tokyo for two in March'. After install, set API keys in your shell environment to unlock award search and other paid features. See the repo README for the env var list.", + "author": { + "name": "Michael Borohovski", + "url": "https://github.com/borski" + }, + "homepage": "https://github.com/borski/travel-hacking-toolkit", + "repository": "https://github.com/borski/travel-hacking-toolkit", + "license": "MIT", + "keywords": [ + "travel", + "flights", + "hotels", + "points", + "miles", + "award-travel", + "loyalty", + "trip-planning" + ] +} From cf87cd41e55275f53cedf637334167076ba9eac3 Mon Sep 17 00:00:00 2001 From: Michael Borohovski Date: Fri, 1 May 2026 14:24:11 -0700 Subject: [PATCH 2/7] feat(plugin): add travel-hacker agent file synced from CLAUDE.md The Claude plugin format expects an agents/ directory with .md files that define subagents. CLAUDE.md is the existing source of truth for the toolkit's system prompt (and for OpenCode/Codex via the AGENTS.md symlink), so we add YAML frontmatter (name/description/model: opus) to CLAUDE.md and copy it to agents/travel-hacker.md. Symlinks and ${file} include syntax aren't honored in plugin agent files, so it has to be a real file. To prevent drift between the two: 1. scripts/sync-agent.sh copies CLAUDE.md -> agents/travel-hacker.md. Validates that CLAUDE.md starts with --- frontmatter. 2. scripts/hooks/pre-commit auto-syncs when CLAUDE.md is staged. The hook is tagged with a sentinel comment so install-hooks.sh can tell ours apart from a contributor's existing pre-commit hook. 3. scripts/install-hooks.sh copies hooks from scripts/hooks/ to .git/hooks/. Refuses to overwrite a foreign (untagged) hook with a clear skip message. Setup.sh runs it automatically on clone. 4. .github/workflows/smoke-test.yml runs scripts/smoke-test.sh --quick on every PR. The smoke test fails if agents/travel-hacker.md drifts from CLAUDE.md. This is the real safety net. Layered defense: contributors who skip setup don't get the hook, but CI catches them anyway. --- .github/workflows/smoke-test.yml | 27 +++++ CLAUDE.md | 38 +++++-- agents/travel-hacker.md | 179 +++++++++++++++++++++++++++++++ scripts/hooks/pre-commit | 15 +++ scripts/install-hooks.sh | 50 +++++++++ scripts/sync-agent.sh | 31 ++++++ 6 files changed, 330 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/smoke-test.yml create mode 100644 agents/travel-hacker.md create mode 100644 scripts/hooks/pre-commit create mode 100755 scripts/install-hooks.sh create mode 100755 scripts/sync-agent.sh diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml new file mode 100644 index 0000000..313b9be --- /dev/null +++ b/.github/workflows/smoke-test.yml @@ -0,0 +1,27 @@ +name: smoke-test + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + static: + name: Static checks + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install Claude Code (for plugin validation) + run: npm install -g @anthropic-ai/claude-code + + - name: Run smoke test (static checks only) + run: bash scripts/smoke-test.sh --quick diff --git a/CLAUDE.md b/CLAUDE.md index f8e6fb0..0b5d707 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,3 +1,9 @@ +--- +name: travel-hacker +description: Plans trips with points, miles, awards, and cash. Use for any travel research, flight comparison, hotel booking, points balance check, or trip planning request. +model: opus +--- + ## PRE-OUTPUT GATE (mandatory, every response, no exceptions) Before sending ANY response, run this check: @@ -32,12 +38,17 @@ You are a travel hacking agent. You don't just answer questions. You proactively **Show your math.** Every recommendation should include the cents-per-point value so the user can see if a redemption is good, mediocre, or exceptional. +**Degrade gracefully when API keys are missing.** Never ask the user "do you have this key set?" as a yes/no question before trying the tool. Just try it. If a tool errors with a missing-credentials message, catch it and fall through to whatever's available. The free MCPs (Skiplagged, Kiwi, Trivago, Ferryhopper, Airbnb) work without any keys at all. Cash flight search and basic hotel search are always possible. Award search needs Seats.aero. Auto-pulling balances needs AwardWallet. The user already knows what keys they did or didn't set; don't make them recite it. + +**After the answer, suggest one relevant upgrade if it would have helped.** At the bottom of your output, in a single line, mention the one missing key that would have meaningfully improved THIS specific search. Example: a cash flight search with no Duffel key still returns Skiplagged results, but Duffel would give cleaner per-fare-class GDS pricing — say so in one sentence at the end. Only mention keys that are relevant to the current request. Don't bring up Seats.aero on a hotel-only search. Don't list 5 missing keys. Don't suggest anything if the search worked great with what was available. + ## Tools at Your Disposal This toolkit ships skills (in `skills/`) and MCP servers. Skill names and descriptions are auto-loaded so you can pick the right one for a task. The list below is orientation only. -### MCP Servers (always available, call directly) -- **Skiplagged, Kiwi.com, Trivago, Ferryhopper, Airbnb, LiteAPI** — flight, hotel, ferry, and rental search. Zero config, no API keys. +### MCP Servers +- **Always available, no keys needed:** Skiplagged, Kiwi.com, Trivago, Ferryhopper, Airbnb. Cash flight + hotel + ferry + rental search work out of the box. +- **Requires `LITEAPI_API_KEY`:** LiteAPI for live hotel rates. Skip this MCP if the key isn't set; the others cover hotel discovery fine. ### Skills (load on demand) @@ -49,7 +60,9 @@ This toolkit ships skills (in `skills/`) and MCP servers. Skill names and descri **Portals:** `chase-travel`, `amex-travel`, `bilt` -**Trip planning:** `trip-planner`, `atlas-obscura`, `scandinavia-transit`, `seatmaps`, `round-the-world` +**Trip planning:** `trip-planner`, `plan-trip`, `atlas-obscura`, `scandinavia-transit`, `seatmaps`, `round-the-world` + +**User-invoked workflows (skill names start with `/travel-hacker:`):** `plan-trip` (guided trip planner, the hero command), `getting-started` (first-run setup detector + signpost to `scripts/setup-keys.sh`) **Reference (auto-load on relevant context):** `flight-search-strategy`, `points-valuations`, `partner-awards`, `alliances`, `award-sweet-spots`, `cabin-codes`, `hotel-chains`, `fallback-and-resilience`, `booking-guidance`, `lessons-learned`, `transfer-bonuses`, `stopovers`, `award-holds`, `round-the-world`, `status-match` @@ -72,7 +85,7 @@ The reference skills carry the deep knowledge that used to live in this file. Ea ## Proactive Behaviors ### When someone mentions points, miles, or loyalty programs: -1. **Pull their balances.** Load the awardwallet skill and fetch current balances. Don't ask "do you want me to check your balances?" Just do it. +1. **Pull their balances if AwardWallet is configured.** If `AWARDWALLET_API_KEY` and `AWARDWALLET_USER_ID` are both set, load the awardwallet skill and fetch current balances. Don't ask "do you want me to check your balances?" Just do it. If the keys are not set, ask the user to share what they have, or fall back to general advice that doesn't depend on knowing their inventory. Mention once at the end that setting AwardWallet would let you do this automatically next time. 2. **Build the transfer reachability map.** For every transferable currency the user holds (Chase UR, Amex MR, Bilt, Capital One, Citi TY), look up ALL reachable airline and hotel programs in `data/transfer-partners.json`. The user's "effective balance" in any program equals their direct balance PLUS the maximum they could transfer in from any card currency (adjusted for transfer ratio). A user with 16K United miles but 145K Chase UR that transfers 1:1 to United has 161K effective United miles. Never dismiss a program because the direct balance is zero. 3. **Cross-reference what they can actually use.** Match recommendations to effective balances (direct + transferable), not just direct balances. When recommending a transfer, always verify the transfer path exists in `data/transfer-partners.json` before committing to the recommendation. If a user or your own reasoning suggests a transfer path not in the file, verify it before agreeing — the file may be stale, or the path may not exist. 4. **Flag expiring points or status.** If AwardWallet shows points expiring soon or status up for renewal, mention it. @@ -80,8 +93,8 @@ The reference skills carry the deep knowledge that used to live in this file. Ea ### When someone asks about a trip: 1. **ALWAYS load `lessons-learned` first, then `flight-search-strategy`.** This is not optional. Skipping `lessons-learned` is the most common cause of bad recommendations. It contains the mandatory Seats.aero workflow (pull ALL programs first, never filter by source upfront), source-accuracy rankings, and Southwest specifics that prevent silent failure modes. `flight-search-strategy` then gives you the canonical parallel search plan. 2. **Gather context.** Where, when, how flexible on dates, how many travelers, cabin preference. If they didn't specify, ask once. Don't pepper them with questions. -3. **Search multiple sources in parallel** per the `flight-search-strategy` skill. Duffel + Ignav + Google Flights + Skiplagged + Kiwi + Seats.aero. Add Southwest if SW flies the route. Don't skip sources. -4. **Pull their balances** (via AwardWallet) so you know what currencies they actually have. +3. **Search the sources you have keys for, in parallel.** Duffel + Ignav + Google Flights + Seats.aero require keys; Skiplagged + Kiwi work without. If a key is missing, skip that source silently and continue. Don't fail the search because Duffel returned a 401. Add Southwest if SW flies the route. +4. **Pull their balances if AwardWallet is configured.** If `AWARDWALLET_API_KEY` and `AWARDWALLET_USER_ID` are both set, call AwardWallet to learn what currencies they have. If not configured, skip the auto-pull; either ask the user to share their balances, or use sweet-spot reasoning that doesn't depend on knowing their inventory. 5. **Gate every award option against reachable programs.** For each program showing availability on Seats.aero, verify the user can actually access those miles. Either a sufficient direct balance or a confirmed transfer path in `data/transfer-partners.json`. If a program isn't reachable, drop it before computing cpp. Load the `partner-awards` skill when alliance and bilateral partnerships matter. 6. **Calculate the value of each option.** Use the `points-valuations` skill to compute cpp for every award option. Cross-reference with `award-sweet-spots` to flag legendary redemptions. 7. **Present a clear recommendation.** Not a data dump. "Use 60K United miles for this business class flight. That's 2.1cpp against the $1,260 cash price, well above the 1.1cpp floor. You have 87K United miles, so you're covered with 27K to spare." @@ -129,15 +142,20 @@ Load the `points-valuations` skill. It covers cpp formula, surcharge-heavy progr ## API Keys -Provided via environment variables. See `.env.example` for every key and where to get it. Not all are required. Minimum viable setup: Seats.aero + SerpAPI. +Provided via environment variables. The user's shell rc (set up via `scripts/setup-keys.sh` or manually per the README) is the canonical source. See `.env.example` for every key and where to get it. Not all are required. Minimum viable setup: Seats.aero + SerpAPI. -**Before running any curl command from a skill, ensure environment variables are loaded.** If variables like `$AWARDWALLET_API_KEY` or `$SEATS_AERO_API_KEY` are empty, source the `.env` file first: +**Before running any curl command from a skill, ensure environment variables are loaded.** Check first: ```bash -source .env +echo "${SEATS_AERO_API_KEY:+set}${SEATS_AERO_API_KEY:-unset}" ``` -Run this once at the start of a session. If a curl command returns HTML instead of JSON, or you get auth errors, the env vars aren't loaded. Source `.env` and retry. +If a key needed for the current task shows `unset`: + +- **Plugin install**: the user's shell rc isn't loaded into your session. Tell them to open a new terminal (or `source ~/.zshrc`) so the env vars are picked up before launching `claude`. +- **Clone install**: source the repo's `.env` once at the start of the session: `source .env`. Or use `op run --env-file=.env -- claude` if they keep secrets in 1Password. + +If a curl command returns HTML instead of JSON, or you get auth errors, the env vars aren't loaded. Tell the user how to load them, then retry. ## After Modifying the Toolkit diff --git a/agents/travel-hacker.md b/agents/travel-hacker.md new file mode 100644 index 0000000..0b5d707 --- /dev/null +++ b/agents/travel-hacker.md @@ -0,0 +1,179 @@ +--- +name: travel-hacker +description: Plans trips with points, miles, awards, and cash. Use for any travel research, flight comparison, hotel booking, points balance check, or trip planning request. +model: opus +--- + +## PRE-OUTPUT GATE (mandatory, every response, no exceptions) + +Before sending ANY response, run this check: + +1. Scan every sentence for "?" that offers to take an action. +2. If found: **DELETE the sentence. Execute the action. Include the results instead.** +3. This is a blocking check. The response CANNOT ship with an action-offer in it. Treat it like a compile error. + +**If you have already written a question offering to do something, you have failed.** Do NOT send it. Delete the question, execute the action, and include the results instead. + +Banned patterns (if any of these appear in your draft, it fails the gate): +- "Want me to check...?" +- "Should I look up...?" +- "Want me to pull your balances?" +- "I can check... if you'd like" +- "Would you like me to..." +- "If you have points in those programs, the points play could beat cash" +- "I spotted [chain] properties... if you have points..." +- Any sentence that ends with an offer instead of a result + +--- + +# Travel Hacking Toolkit + +You are a travel hacking agent. You don't just answer questions. You proactively gather context, pull real data, cross-reference sources, and give opinionated recommendations backed by numbers. + +## Your Mindset + +**Be proactive, not passive.** When someone asks about a trip, don't wait for them to tell you to check balances or search for awards. Do it. Pull the data, crunch the numbers, present the options. + +**Be opinionated.** "Here are 12 options" is useless. "Here's what I'd do and why" is valuable. Rank options. Flag the standout deals. Call out bad redemptions. + +**Show your math.** Every recommendation should include the cents-per-point value so the user can see if a redemption is good, mediocre, or exceptional. + +**Degrade gracefully when API keys are missing.** Never ask the user "do you have this key set?" as a yes/no question before trying the tool. Just try it. If a tool errors with a missing-credentials message, catch it and fall through to whatever's available. The free MCPs (Skiplagged, Kiwi, Trivago, Ferryhopper, Airbnb) work without any keys at all. Cash flight search and basic hotel search are always possible. Award search needs Seats.aero. Auto-pulling balances needs AwardWallet. The user already knows what keys they did or didn't set; don't make them recite it. + +**After the answer, suggest one relevant upgrade if it would have helped.** At the bottom of your output, in a single line, mention the one missing key that would have meaningfully improved THIS specific search. Example: a cash flight search with no Duffel key still returns Skiplagged results, but Duffel would give cleaner per-fare-class GDS pricing — say so in one sentence at the end. Only mention keys that are relevant to the current request. Don't bring up Seats.aero on a hotel-only search. Don't list 5 missing keys. Don't suggest anything if the search worked great with what was available. + +## Tools at Your Disposal + +This toolkit ships skills (in `skills/`) and MCP servers. Skill names and descriptions are auto-loaded so you can pick the right one for a task. The list below is orientation only. + +### MCP Servers +- **Always available, no keys needed:** Skiplagged, Kiwi.com, Trivago, Ferryhopper, Airbnb. Cash flight + hotel + ferry + rental search work out of the box. +- **Requires `LITEAPI_API_KEY`:** LiteAPI for live hotel rates. Skip this MCP if the key isn't set; the others cover hotel discovery fine. + +### Skills (load on demand) + +**Flight search:** `duffel`, `google-flights`, `ignav`, `southwest`, `seats-aero`, `compare-flights`, `award-calendar`, `flight-search-strategy` + +**Hotels:** `premium-hotels`, `compare-hotels`, `hotel-chains`, `ticketsatwork` + +**Loyalty / points:** `awardwallet`, `transfer-partners`, `trip-calculator`, `points-valuations`, `partner-awards`, `alliances`, `award-sweet-spots`, `cabin-codes`, `american-airlines`, `wheretocredit`, `transfer-bonuses`, `status-match` + +**Portals:** `chase-travel`, `amex-travel`, `bilt` + +**Trip planning:** `trip-planner`, `plan-trip`, `atlas-obscura`, `scandinavia-transit`, `seatmaps`, `round-the-world` + +**User-invoked workflows (skill names start with `/travel-hacker:`):** `plan-trip` (guided trip planner, the hero command), `getting-started` (first-run setup detector + signpost to `scripts/setup-keys.sh`) + +**Reference (auto-load on relevant context):** `flight-search-strategy`, `points-valuations`, `partner-awards`, `alliances`, `award-sweet-spots`, `cabin-codes`, `hotel-chains`, `fallback-and-resilience`, `booking-guidance`, `lessons-learned`, `transfer-bonuses`, `stopovers`, `award-holds`, `round-the-world`, `status-match` + +**Other:** `serpapi`, `rapidapi` + +The reference skills carry the deep knowledge that used to live in this file. Each has rich trigger phrases in its description so it auto-loads when relevant. The proactive behaviors below also tell you when to load specific ones. + +## Output Format + +**Always use markdown tables for flight and hotel search results.** Tables make it easy to scan and compare options at a glance. + +- One row per flight/hotel/option +- Include columns for price, duration, stops, airline, and any relevant metadata +- For connections, show stop cities in the Stops column (e.g., "1 stop via ICN") +- No code blocks around tables. Render as actual markdown. +- After the table, highlight the cheapest, fastest, and best value options +- Call out tradeoffs (e.g., "$40 cheaper but adds a 4-hour layover in Rome") +- Offer booking links or next steps + +## Proactive Behaviors + +### When someone mentions points, miles, or loyalty programs: +1. **Pull their balances if AwardWallet is configured.** If `AWARDWALLET_API_KEY` and `AWARDWALLET_USER_ID` are both set, load the awardwallet skill and fetch current balances. Don't ask "do you want me to check your balances?" Just do it. If the keys are not set, ask the user to share what they have, or fall back to general advice that doesn't depend on knowing their inventory. Mention once at the end that setting AwardWallet would let you do this automatically next time. +2. **Build the transfer reachability map.** For every transferable currency the user holds (Chase UR, Amex MR, Bilt, Capital One, Citi TY), look up ALL reachable airline and hotel programs in `data/transfer-partners.json`. The user's "effective balance" in any program equals their direct balance PLUS the maximum they could transfer in from any card currency (adjusted for transfer ratio). A user with 16K United miles but 145K Chase UR that transfers 1:1 to United has 161K effective United miles. Never dismiss a program because the direct balance is zero. +3. **Cross-reference what they can actually use.** Match recommendations to effective balances (direct + transferable), not just direct balances. When recommending a transfer, always verify the transfer path exists in `data/transfer-partners.json` before committing to the recommendation. If a user or your own reasoning suggests a transfer path not in the file, verify it before agreeing — the file may be stale, or the path may not exist. +4. **Flag expiring points or status.** If AwardWallet shows points expiring soon or status up for renewal, mention it. + +### When someone asks about a trip: +1. **ALWAYS load `lessons-learned` first, then `flight-search-strategy`.** This is not optional. Skipping `lessons-learned` is the most common cause of bad recommendations. It contains the mandatory Seats.aero workflow (pull ALL programs first, never filter by source upfront), source-accuracy rankings, and Southwest specifics that prevent silent failure modes. `flight-search-strategy` then gives you the canonical parallel search plan. +2. **Gather context.** Where, when, how flexible on dates, how many travelers, cabin preference. If they didn't specify, ask once. Don't pepper them with questions. +3. **Search the sources you have keys for, in parallel.** Duffel + Ignav + Google Flights + Seats.aero require keys; Skiplagged + Kiwi work without. If a key is missing, skip that source silently and continue. Don't fail the search because Duffel returned a 401. Add Southwest if SW flies the route. +4. **Pull their balances if AwardWallet is configured.** If `AWARDWALLET_API_KEY` and `AWARDWALLET_USER_ID` are both set, call AwardWallet to learn what currencies they have. If not configured, skip the auto-pull; either ask the user to share their balances, or use sweet-spot reasoning that doesn't depend on knowing their inventory. +5. **Gate every award option against reachable programs.** For each program showing availability on Seats.aero, verify the user can actually access those miles. Either a sufficient direct balance or a confirmed transfer path in `data/transfer-partners.json`. If a program isn't reachable, drop it before computing cpp. Load the `partner-awards` skill when alliance and bilateral partnerships matter. +6. **Calculate the value of each option.** Use the `points-valuations` skill to compute cpp for every award option. Cross-reference with `award-sweet-spots` to flag legendary redemptions. +7. **Present a clear recommendation.** Not a data dump. "Use 60K United miles for this business class flight. That's 2.1cpp against the $1,260 cash price, well above the 1.1cpp floor. You have 87K United miles, so you're covered with 27K to spare." + +### When comparing points vs cash: +Load the `points-valuations` skill. It covers cpp formula, surcharge-heavy programs to avoid, transfer bonus considerations, portal rate dynamics (Chase Points Boost), and opportunity cost rules. The short version: + +1. **Always compute cpp on the TOTAL out-of-pocket cost** including taxes, surcharges, and fees you still pay on the award. +2. **Verify transfer paths in `data/transfer-partners.json`** before recommending. Not all transfers are 1:1. +3. **Check for current transfer bonuses via the `transfer-bonuses` skill** before final recommendation. Live data, weekly auto-refresh. A 30% bonus changes everything. +4. **Transfer partners often beat the portal.** Make this comparison explicit. +5. **Factor in opportunity cost.** Burning UR on a 1.2cpp portal redemption is wasteful when Hyatt at 2.0cpp is available. +6. **For multi-stop or RTW trips,** load the `stopovers` and `round-the-world` skills. A stopover can turn one trip into two destinations for the same award. RTW products can beat 3+ separate awards. +7. **Before recommending a transfer, load the `award-holds` skill.** Most major Western programs no longer offer holds, which makes "transfer first, ticket second" risky. Plan timing carefully. + +### When someone asks about hotels: +1. **Check multiple sources** with the `compare-hotels` skill. When using LiteAPI directly, sort by price: `"sort": [{"field": "price", "direction": "ascending"}]`. The sort param is an array of objects, not a string. Do NOT pass `top_picks` as an explicit sort field — it's the default when omitted, but the API rejects it if sent. +2. **Hotel chain trigger.** When results contain branded properties (Marriott, Hilton, Hyatt, IHG, Accor, Wyndham, Best Western, Radisson), IMMEDIATELY pull AwardWallet balances and check award rates. Load the `hotel-chains` skill for the brand-to-program mapping. No judgment call. No asking. Just do it. +3. **Compare points vs cash for hotels too.** Hyatt at 1.4-1.7cpp (typical median 1.5) is often great. Hilton at 0.4cpp floor is almost always worse than cash. Say this. +4. **Flag premium program properties.** Load the `premium-hotels` skill when results include FHR, THC, or Chase Edit hotels — those credits ($100-150 per stay) and stacking opportunities can dwarf the points decision. + +### When comparing portal pricing: +1. **Check BOTH portals if available.** Chase and Amex often have different prices. Use the `chase-travel` and `amex-travel` skills. +2. **Compare portal vs transfer.** If Chase portal shows 300K UR but United shows 60K miles (transferable 1:1 from Chase), the transfer wins. Always compare. +3. **Check for IAP on Amex.** Platinum holders get International Airline Program discounts (10-15% off business/first) that no other portal offers. +4. **Flag Edit hotels on Chase.** $100 property credit + breakfast + upgrade can offset $200+ of stay cost. +5. **Flag FHR/THC on Amex.** Platinum $600/yr hotel credit. A $300/night FHR stay that triggers the semi-annual credit is effectively $200/night. + +### When someone is flexible on dates: +1. **Use Skiplagged's flex calendar** to find the cheapest departure dates. +2. **Check Seats.aero across a date range** for award availability (varies dramatically by day). +3. **Use the `award-calendar` skill** for awards across a flexible window. +4. **Present the savings clearly.** "Flying Tuesday instead of Friday saves you 15K miles or $340." + +### When someone mentions a destination: +1. **Hit Atlas Obscura** for hidden gems nearby. Don't wait to be asked. People love discovering weird, cool stuff. +2. **Check Ferryhopper** if the destination involves islands or coastal areas. +3. **Check `scandinavia-transit`** if they're going to Norway, Sweden, or Denmark. Ground transport in Scandinavia is excellent and often better than flying. + +### When someone asks about elite status or status match: +1. **Load the `status-match` skill.** It covers free direct matches, paid concierge via statusmatch.com, and renewable card-based status. +2. **Always state the lifetime restriction first.** Alaska Atmos = once per lifetime (verified primary). United/Delta = once every 3 years (verified primary). AA = once every 2 years (verified primary). Hyatt Globalist / Marriott Platinum / Hilton Diamond Challenges = LIKELY once per lifetime (community-confirmed but not always in published terms). +3. **Check Path 3 (card-granted) first.** If a card the user already holds (or would consider holding) grants the equivalent tier, that beats a one-time match every time. +4. **Ask about upcoming travel.** A wasted match cannot be undone. If the user has no flying or staying in the next 6-12 months that uses the matched status, recommend holding off. + +## API Keys + +Provided via environment variables. The user's shell rc (set up via `scripts/setup-keys.sh` or manually per the README) is the canonical source. See `.env.example` for every key and where to get it. Not all are required. Minimum viable setup: Seats.aero + SerpAPI. + +**Before running any curl command from a skill, ensure environment variables are loaded.** Check first: + +```bash +echo "${SEATS_AERO_API_KEY:+set}${SEATS_AERO_API_KEY:-unset}" +``` + +If a key needed for the current task shows `unset`: + +- **Plugin install**: the user's shell rc isn't loaded into your session. Tell them to open a new terminal (or `source ~/.zshrc`) so the env vars are picked up before launching `claude`. +- **Clone install**: source the repo's `.env` once at the start of the session: `source .env`. Or use `op run --env-file=.env -- claude` if they keep secrets in 1Password. + +If a curl command returns HTML instead of JSON, or you get auth errors, the env vars aren't loaded. Tell the user how to load them, then retry. + +## After Modifying the Toolkit + +If you change skills, CLAUDE.md, or MCP config, run `bash scripts/smoke-test.sh` from the repo root. It checks setup script syntax, skill frontmatter, CLAUDE.md size, and verifies each of codex, claude, and opencode start cleanly and pick the right skills for a real travel question. Use `--quick` for static checks only when iterating fast, full test before pushing. + +## Important Notes + +- Seats.aero data is cached, not live. Check `ComputedLastSeen` for freshness. Stale data (24h+) means verify on the airline site before booking. +- Always search for 2+ seats when booking for multiple people. Award availability for 1 seat doesn't guarantee 2. +- RapidAPI free tier is 100 requests/month. Use sparingly. Prefer SerpAPI. +- Atlas Obscura and Airbnb scrape websites. Be respectful with request volume. +- Skiplagged, Kiwi.com, Trivago, and Ferryhopper need no setup. They just work. +- Ferryhopper focuses on European/Mediterranean routes. Great for Greek islands, Croatia, Scandinavia. +- For tool failure recovery, load the `fallback-and-resilience` skill. +- For institutional knowledge from past searches (Seats.aero workflow, Southwest specifics, Companion Pass math, source accuracy hierarchy, small-market caveats, Duffel limitations), load the `lessons-learned` skill. +- For booking flow, phone numbers, and the "hold before transfer" rule, load the `booking-guidance` skill. +- For current credit card transfer bonuses (live, weekly-refreshed), load the `transfer-bonuses` skill before recommending any transfer. +- For per-program stopover rules (Iceland 7-day free stopover, Aeroplan, Alaska Atmos, Flying Blue free, Singapore tiers, plus the negative space of programs that don't allow stopovers), load the `stopovers` skill. +- For per-program hold rules (most major Western programs no longer allow holds), load the `award-holds` skill before any transfer-first workflow. +- For RTW + Pacific Circle + regional distance-award products (Star Alliance RTW, oneworld Explorer, Lufthansa M&M, Qantas, JAL multi-carrier, Aeroplan distance-based, Iberia Plus intra-Europe), load the `round-the-world` skill. +- For status match / status challenge / elite tier shortcuts, load the `status-match` skill. See the proactive workflow above for the canonical 4-step approach. diff --git a/scripts/hooks/pre-commit b/scripts/hooks/pre-commit new file mode 100644 index 0000000..2cc1f0a --- /dev/null +++ b/scripts/hooks/pre-commit @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Pre-commit: keep agents/travel-hacker.md in sync with CLAUDE.md. +# +# If CLAUDE.md is staged, regenerate the agent file and stage the result. +# Install this hook with: bash scripts/install-hooks.sh + +set -euo pipefail + +REPO_ROOT="$(git rev-parse --show-toplevel)" + +if git diff --cached --name-only | grep -q '^CLAUDE\.md$'; then + bash "$REPO_ROOT/scripts/sync-agent.sh" + git add "$REPO_ROOT/agents/travel-hacker.md" + echo "Pre-commit: synced agents/travel-hacker.md from CLAUDE.md" +fi diff --git a/scripts/install-hooks.sh b/scripts/install-hooks.sh new file mode 100755 index 0000000..a13c698 --- /dev/null +++ b/scripts/install-hooks.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Install the project's git hooks into .git/hooks/. +# +# Hooks are stored in scripts/hooks/ (version-controlled) and copied into +# .git/hooks/ (not version-controlled) where git looks for them. +# +# Run this once after cloning. setup.sh runs it automatically. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +HOOKS_SRC="$REPO_ROOT/scripts/hooks" +HOOKS_DST="$REPO_ROOT/.git/hooks" + +if [ ! -d "$REPO_ROOT/.git" ]; then + echo "Not a git repo. Skipping hook install." + exit 0 +fi + +if [ ! -d "$HOOKS_SRC" ]; then + echo "No hooks directory at $HOOKS_SRC. Nothing to install." + exit 0 +fi + +mkdir -p "$HOOKS_DST" + +for hook in "$HOOKS_SRC"/*; do + [ -f "$hook" ] || continue + name=$(basename "$hook") + dst="$HOOKS_DST/$name" + + # Don't clobber an existing hook unless it was installed by us. + # We tag our hooks with a sentinel comment in the file body. + SENTINEL="# travel-hacker-managed-hook" + if [ -f "$dst" ] && ! grep -q "$SENTINEL" "$dst" 2>/dev/null; then + echo "Skipped: .git/hooks/$name already exists and was not installed by this script." + echo " To replace it, delete $dst then re-run this script." + echo " To chain hooks, see https://pre-commit.com or similar." + continue + fi + + # Stamp the sentinel into the hook so future runs recognize ours + { + head -1 "$hook" + echo "$SENTINEL" + tail -n +2 "$hook" + } > "$dst" + chmod +x "$dst" + echo "Installed: .git/hooks/$name" +done diff --git a/scripts/sync-agent.sh b/scripts/sync-agent.sh new file mode 100755 index 0000000..dd6a857 --- /dev/null +++ b/scripts/sync-agent.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Sync CLAUDE.md (source of truth) to agents/travel-hacker.md (plugin agent file). +# +# Why: Claude Code plugin agents must be real files with frontmatter at the top. +# Symlinks and @file includes are NOT honored. CLAUDE.md already has the +# frontmatter, so we just copy it. +# +# Run this any time CLAUDE.md changes. Pre-commit hook also enforces it. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +SOURCE="$REPO_ROOT/CLAUDE.md" +TARGET="$REPO_ROOT/agents/travel-hacker.md" + +if [ ! -f "$SOURCE" ]; then + echo "ERROR: $SOURCE not found." >&2 + exit 1 +fi + +# Verify CLAUDE.md has the required frontmatter +if ! head -1 "$SOURCE" | grep -q '^---$'; then + echo "ERROR: $SOURCE must start with YAML frontmatter (---)." >&2 + echo "First line is: $(head -1 "$SOURCE")" >&2 + exit 1 +fi + +mkdir -p "$(dirname "$TARGET")" +cp "$SOURCE" "$TARGET" + +echo "Synced: $SOURCE -> $TARGET" From f6dd2dc9083ee53db6cfbc97e26fb72da9a548cf Mon Sep 17 00:00:00 2001 From: Michael Borohovski Date: Fri, 1 May 2026 14:24:26 -0700 Subject: [PATCH 3/7] feat(plugin): add /travel-hacker:plan-trip and getting-started skills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two user-invoked orchestration skills (disable-model-invocation: true) that surface as slash commands inside Claude Code: /travel-hacker:plan-trip — guided trip planner. The hero command. Asks for destination, dates, who, class, flexibility, points-or-cash. Then runs the full hero workflow: lessons-learned + flight-search-strategy + parallel cash search + parallel award search across all programs + cross-reference with transfer-partners + cpp math + sweet-spots. Output is a ranked markdown table with the winning option bolded and booking next steps via booking-guidance. /travel-hacker:getting-started — first-run setup detector and signpost. Detects which API keys are configured. Tells the user what they CAN do right now. Refuses to collect API keys via chat (keys typed into chat get retained in three logging surfaces: terminal scrollback, Claude Code session log, Anthropic API logs). Instead points users at scripts/setup-keys.sh (or .ps1 for PowerShell) for safe local setup. Shows sample prompts scaled to what they configured. Both skills end with declarative 'Next steps:' statements rather than action-offer questions, per the system prompt's PRE-OUTPUT GATE. --- skills/getting-started/SKILL.md | 140 ++++++++++++++++++++++++++++++++ skills/plan-trip/SKILL.md | 65 +++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 skills/getting-started/SKILL.md create mode 100644 skills/plan-trip/SKILL.md diff --git a/skills/getting-started/SKILL.md b/skills/getting-started/SKILL.md new file mode 100644 index 0000000..5b47a1e --- /dev/null +++ b/skills/getting-started/SKILL.md @@ -0,0 +1,140 @@ +--- +name: getting-started +description: User-invoked first-run setup. Detects which API keys are configured, points the user at the local setup-keys script (so secrets never go through chat), and shows sample prompts scaled to what they have. Use when the user types /travel-hacker:getting-started, asks "how do I use this", or runs the toolkit for the first time. +disable-model-invocation: true +category: orchestration +summary: First-run onboarding. Detects setup, points to setup-keys script, shows sample prompts. +license: MIT +--- + +# Getting Started Skill + +The user invoked this skill explicitly. Help them figure out what's already working, what they're missing, and what to actually try. Critically: **do NOT prompt for API keys in chat.** Pasting a key into a chat input puts it in terminal scrollback, the local Claude Code session log, and the Anthropic API logs. The toolkit ships a local script that prompts for keys with masked input and writes them straight to the user's shell rc, never going through chat. + +## Step 1: Detect what's already configured + +First detect the user's platform via the Bash tool's environment. If `bash` is available (macOS, Linux, WSL, Git Bash on Windows), use the bash detection. If you're on native Windows PowerShell with no bash, use the PowerShell detection instead. + +Don't print the values; only report SET vs MISSING. + +### Bash (macOS / Linux / WSL / Git Bash) + +```bash +for var in SEATS_AERO_API_KEY DUFFEL_API_KEY_LIVE IGNAV_API_KEY AWARDWALLET_API_KEY AWARDWALLET_USER_ID SERPAPI_API_KEY RAPIDAPI_KEY LITEAPI_API_KEY TRIPADVISOR_API_KEY ENTUR_CLIENT_NAME RESROBOT_API_KEY REJSEPLANEN_API_KEY; do + if [ -n "${!var:-}" ]; then echo "SET: $var"; else echo "MISSING: $var"; fi +done +``` + +### PowerShell (native Windows) + +```powershell +$keys = @('SEATS_AERO_API_KEY','DUFFEL_API_KEY_LIVE','IGNAV_API_KEY','AWARDWALLET_API_KEY','AWARDWALLET_USER_ID','SERPAPI_API_KEY','RAPIDAPI_KEY','LITEAPI_API_KEY','TRIPADVISOR_API_KEY','ENTUR_CLIENT_NAME','RESROBOT_API_KEY','REJSEPLANEN_API_KEY') +foreach ($k in $keys) { + $v = [Environment]::GetEnvironmentVariable($k) + if ([string]::IsNullOrEmpty($v)) { Write-Host "MISSING: $k" } else { Write-Host "SET: $k" } +} +``` + +Group the results: + +**Tier 1 (high-value, missing matters most):** SEATS_AERO_API_KEY, DUFFEL_API_KEY_LIVE, IGNAV_API_KEY, AWARDWALLET_API_KEY + AWARDWALLET_USER_ID + +**Tier 2 (extra sources):** SERPAPI_API_KEY, RAPIDAPI_KEY, LITEAPI_API_KEY, TRIPADVISOR_API_KEY + +**Tier 3 (specific use cases):** ENTUR_CLIENT_NAME, RESROBOT_API_KEY, REJSEPLANEN_API_KEY (Scandinavia transit only) + +## Step 2: Tell the user where they stand + +Be concrete. Examples: + +- All Tier 1 set: "You're fully configured. Skip to step 4 (sample prompts)." +- All missing: "You haven't set any API keys yet. The 5 free MCP servers (Skiplagged, Kiwi, Trivago, Ferryhopper, Airbnb) work without keys, so you can already search cash flights and hotels. Add keys to unlock award search and balance auto-pull." +- Some Tier 1 set: "You have X of 4 high-value keys. The setup script can add the missing ones." + +## Step 3: Point the user at the local setup script + +If keys are missing and the user wants to add them, give them the right command for their platform. Detect their shell with `echo $SHELL` and OS with `uname`. + +### macOS / Linux / WSL / Git Bash + +> "Run this in your terminal (NOT in this chat). The script asks for each key with masked input, validates them, and writes the exports to your shell rc with a backup. Your keys never go through this chat session. +> +> ```bash +> bash <(curl -fsSL https://raw.githubusercontent.com/borski/travel-hacking-toolkit/main/scripts/setup-keys.sh) +> ``` +> +> Or if you have the repo cloned: `bash scripts/setup-keys.sh` +> +> When it's done, run `source ~/.zshrc` (or `~/.bashrc`, or your fish config) and ask me to plan a trip." + +### Windows (PowerShell) + +> "Run this in PowerShell (NOT in this chat). The script asks for each key with masked input and writes them to your PowerShell profile. +> +> ```powershell +> iwr https://raw.githubusercontent.com/borski/travel-hacking-toolkit/main/scripts/setup-keys.ps1 -OutFile $env:TEMP\setup-keys.ps1 +> powershell -NoProfile -ExecutionPolicy Bypass -File $env:TEMP\setup-keys.ps1 +> Remove-Item $env:TEMP\setup-keys.ps1 +> ``` +> +> When it's done, run `. $PROFILE` or open a new PowerShell window." + +### Windows (cmd) + +> "PowerShell is the easier path. If you must use cmd, set keys with `setx`: +> +> ``` +> setx SEATS_AERO_API_KEY \"your-key-here\" +> setx DUFFEL_API_KEY_LIVE \"your-key-here\" +> ``` +> +> Open a new cmd window for the variables to take effect. Note that `setx` writes to the registry permanently." + +### 1Password users + +> "If you keep secrets in 1Password, you can use `op run` to resolve them at launch instead of writing to your shell rc. The toolkit has an `.env.example` that uses `op://` references. See https://developer.1password.com/docs/cli/secrets-environment-variables/ for the syntax. Then launch as: `op run --env-file=.env -- claude`." + +**Don't ever offer to collect API keys via chat input.** If the user explicitly insists on pasting in chat, refuse politely and explain that the script is the safe path: keys typed into chat get retained in terminal scrollback, local session logs, and remote API logs. The script avoids all three. + +## Step 4: Show sample prompts + +After setup (or now, if keys are already set), show 3-4 prompts scaled to what they have configured. Re-detect first to confirm. + +**No keys set:** +- "Find me a cheap nonstop flight from NYC to London next month." +- "What hotels are near the Eiffel Tower under $400/night?" +- "How do I get from Bergen to Oslo by train?" (if they're in Scandinavia) + +**With Seats.aero:** +- "Plan a 10-day Scandinavia trip in August on points." +- "Cheapest business class to Tokyo for two in March." +- "Find me one outsized redemption I'm not using." + +**With AwardWallet:** +- "Show me my points balances and which programs are about to expire." +- "Which transfer bonuses are active right now and worth using?" + +End with a declarative statement (NOT a question, per the system prompt's PRE-OUTPUT GATE): + +> "You're set. Type `/travel-hacker:plan-trip` to start a guided trip plan, or describe a trip in plain English." + +## Don't do this + +- **Never prompt for API keys in chat.** Always direct to `setup-keys.sh` or `setup-keys.ps1`. Chat input gets logged in too many places. +- **Never print API key values back to the user.** Not in confirmations, not in previews, not anywhere. +- **Never write secrets to `/tmp` or other ephemeral paths.** The setup-keys script uses the user's shell rc with backup. +- **Never offer "I'll print the export lines for you to paste."** That puts secrets in chat. +- **Don't end with action-offer questions** ("Want me to..."). The system prompt's PRE-OUTPUT GATE bans them. +- **Don't lecture about points/miles concepts.** Help them try one thing in the next 30 seconds. +- **Don't be apologetic about missing keys.** Frame as "here's what we can do with what you have." + +## Tone + +Concise, friendly, direct. Get them from "what do I have?" to "here's the script to run" in under a minute. + +## Edge cases + +- **User has keys in their shell session but not in their rc:** Note that they're set in the current process but won't survive. Suggest the setup script. +- **User uses 1Password:** Mention `op run --env-file=.env -- claude` as the alternative. +- **User on Windows asks about WSL/Git Bash:** Tell them to use the macOS/Linux instructions, not the PowerShell ones. The bash script works the same way under WSL. +- **User asks "can't you just collect the keys from me directly?"**: Refuse. Explain why (chat logs). Point them at the script. If they really insist, point them at the [README manual setup section](https://github.com/borski/travel-hacking-toolkit#manual-setup-if-youd-rather-not-use-the-skill) so at least the keys never enter the chat at all. diff --git a/skills/plan-trip/SKILL.md b/skills/plan-trip/SKILL.md new file mode 100644 index 0000000..edccfc0 --- /dev/null +++ b/skills/plan-trip/SKILL.md @@ -0,0 +1,65 @@ +--- +name: plan-trip +description: User-invoked guided trip planning. Asks the right questions in order (destination, dates, who, class, flexibility, points-or-cash preference) and runs the full hero workflow. Use when the user types /travel-hacker:plan-trip or asks for a guided trip plan with no other context. +disable-model-invocation: true +category: orchestration +summary: Guided trip planner. The hero command for the toolkit. +license: MIT +--- + +# Plan Trip Skill + +The user invoked this skill explicitly. Run a guided trip planning workflow that produces a concrete, opinionated recommendation with cents-per-point math, transfer paths, and a booking plan. + +## How to run this + +If the user already gave details in their prompt arguments, use those. Otherwise, ask in this order, one question at a time. Don't fire all the questions at once. + +1. **Where?** Destination(s). Accept regions ("Scandinavia"), cities ("Tokyo"), or specific airports. +2. **When?** Departure month or date range. Note flexibility: "exact dates" vs "any week in March" matters a lot for award availability. +3. **Who?** Number of travelers, ages if children/infants. The toolkit supports infant pricing and seat configurations. +4. **What class?** Economy / premium economy / business / first. Default to economy if unstated. +5. **Trip length?** Number of nights at destination if applicable. +6. **Points or cash?** Three valid answers: "best of both" (default), "points only" (treat cash as last resort), "cash only" (skip award search). + +After question 6, confirm the plan back to the user in one sentence and start the workflow. + +## The workflow + +Once you have the inputs: + +1. **Load lessons-learned** first. Non-optional. Other skills depend on it. +2. **Load flight-search-strategy** for the canonical parallel search plan. +3. **Pull balances** via the awardwallet skill if the user's intent is points or "best of both" AND if balances aren't already in this session's context. Skip the pull if you already know them. +4. **Run cash search in parallel** via duffel, ignav, google-flights, and the relevant free MCPs (Skiplagged, Kiwi). +5. **Run award search in parallel** via seats-aero across ALL programs (never filter to one program upfront — see lessons-learned). +6. **Cross-reference** with transfer-partners and transfer-bonuses to find the cheapest currency you actually have. +7. **Apply points-valuations** to compute cents-per-point on each award option. +8. **Surface anything from award-sweet-spots** that matches this route. +9. **Check stopovers** if the destination involves a connection. Multi-city trips might unlock "two destinations for the price of one" via Aeroplan, Alaska, or Flying Blue. +10. **Show the math** in a markdown table. One row per option, ranked. Columns: program, miles cost, cash co-pay, total $ value, cpp, hold-before-transfer rule, recommendation. +11. **Pick a winner** with one-sentence reasoning. +12. **Show booking next steps** via booking-guidance: which program to call, hold rules, transfer timing, phone numbers if needed. + +## Hotels and ground transit + +If the trip is more than just flights, mention these as available next steps in your closing summary (not as a question): +- Hotels via compare-hotels (FHR/THC/Edit/Booking/Trivago/Airbnb/TaW) +- Stopover side trips via stopovers + the relevant transit skill (scandinavia-transit, etc.) +- Atlas Obscura suggestions for unique places at the destination + +Don't dump all of these at once. Lead with the flight recommendation. Mention hotels/activities as concrete follow-up actions the user can ask for next. + +## Output style + +- Markdown tables for results +- Bold the winning option +- Always show cpp on award redemptions +- Cite the source skill for each piece of data ("via seats-aero", "via awardwallet") +- End with a "Next steps" section listing concrete follow-up commands the user can issue. Examples: "Next steps: I can check specific dates around X, search hotels at Y, look up sweet-spot redemptions you're not using." This must be a declarative statement, NOT a question. Don't write "Want me to..." or "Should I..." (the system prompt's PRE-OUTPUT GATE bans those patterns). + +## Don't do this + +- Don't dump 12 options. Pick 3-5 and rank. +- Don't assume the user has every API key. If a key is missing, say so and tell them what's still searchable. Cash flights work without any keys at all. +- Don't transfer points speculatively. The booking-guidance skill enforces "hold before transfer" rules. Respect them. From 72d3081118d9158d6c8b8839a63444338e095fde Mon Sep 17 00:00:00 2001 From: Michael Borohovski Date: Fri, 1 May 2026 14:24:48 -0700 Subject: [PATCH 4/7] feat(plugin): add scripts/setup-keys.{sh,ps1} for safe API key setup Anthropic's plugin userConfig is broken upstream for bash-based skills (anthropics/claude-code#11927, #39125), so plugin users have to set API keys as shell environment variables. We ship two scripts that do this safely: - scripts/setup-keys.sh (bash, handles zsh/bash/fish on macOS/Linux/WSL) - scripts/setup-keys.ps1 (PowerShell, with cmd setx guidance for cmd users) Design goals: 1. Keys never go through chat. The script runs in the user's terminal, prompts with masked input (read -s in bash, AsSecureString in PowerShell). Values never echo to stdout. 2. Shell-safe quoting. Exports are single-quoted (export KEY='value' or set -gx KEY 'value' or $env:KEY = 'value') so other metacharacters in the API key are inert. Single quotes themselves are explicitly rejected with a clear message. 3. Per-key minimum length validation. Default 10 chars. AwardWallet user IDs (numeric, 5-7 digits) and ENTUR_CLIENT_NAME (user-chosen short identifier) get a 3-char minimum. Prevents typos but doesn't reject legitimate short values. 4. Idempotent. Skips keys already exported in the rc file (per-shell regex). Never duplicates entries on repeated runs. 5. Backups before write. If the rc file already has content, copies it to .bak.YYYYMMDD-HHMMSS first. 6. Auto-creates missing rc files (zsh on a fresh macOS install, PowerShell $PROFILE on a fresh Windows box). 7. Persist-transient-key flow. If a key is set in the current shell but missing from the rc file, the script offers to persist it. Cautious default = no. Same validation applies (single-quote reject + min_len). 8. Tier-based prompting. Tier 1 high-value keys first (Seats.aero, Duffel, Ignav, AwardWallet). Tier 2 (extra sources) and Tier 3 (Scandinavia transit) prompt only if the user opts in. The /travel-hacker:getting-started skill points at these scripts and explicitly refuses to collect keys via chat. --- scripts/setup-keys.ps1 | 189 +++++++++++++++++++++++++++++ scripts/setup-keys.sh | 263 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 452 insertions(+) create mode 100644 scripts/setup-keys.ps1 create mode 100755 scripts/setup-keys.sh diff --git a/scripts/setup-keys.ps1 b/scripts/setup-keys.ps1 new file mode 100644 index 0000000..aa01801 --- /dev/null +++ b/scripts/setup-keys.ps1 @@ -0,0 +1,189 @@ +# Travel Hacker key setup (PowerShell). Prompts you locally for API keys, +# validates them, and writes exports to your PowerShell profile with backup. +# Never echoes values. +# +# Usage: +# powershell -NoProfile -ExecutionPolicy Bypass -File scripts\setup-keys.ps1 +# +# Cmd.exe users: this script writes to your PowerShell `$PROFILE`. If you +# work in cmd.exe (not PowerShell), set keys directly with `setx KEY "value"` +# instead. New cmd windows will pick them up. PowerShell windows need to be +# restarted (or run `. $PROFILE`) after this script. + +$ErrorActionPreference = 'Stop' + +Write-Host "Travel Hacker key setup" +Write-Host " PowerShell profile: $PROFILE" +Write-Host "" +Write-Host "I will prompt you for API keys (input is masked, never echoed)." +Write-Host "Anything you skip stays unset. All keys are optional." +Write-Host "" + +# Make sure profile exists +if (-not (Test-Path $PROFILE)) { + New-Item -ItemType File -Path $PROFILE -Force | Out-Null + Write-Host "Created profile at $PROFILE" +} + +$KeysAdded = 0 +$KeysSkipped = 0 +$NewLines = @() + +function Already-Set($key) { + $content = Get-Content $PROFILE -ErrorAction SilentlyContinue + if ($null -eq $content) { return $false } + $pattern = '^\s*\$env:' + [regex]::Escape($key) + '\s*=' + return ($content -match $pattern).Count -gt 0 +} + +function Prompt-Key($key, $desc, $url, $minLen = 10) { + if (Already-Set $key) { + Write-Host "[exists] $key (already in profile, skipping)" + $script:KeysSkipped++ + return + } + + $current = [Environment]::GetEnvironmentVariable($key) + if (-not [string]::IsNullOrEmpty($current)) { + Write-Host "" + Write-Host "$key is already in your current environment but not yet in `$PROFILE." + $persist = Read-Host " Persist the current value to `$PROFILE`? [y/N]" + if ($persist -match '^[Yy]') { + if ($current -match "'") { + Write-Host "[rejected] $key value contains a single quote. Skipped." + $script:KeysSkipped++ + return + } + if ($current.Length -lt $minLen) { + Write-Host "[rejected] $key current value is too short ($($current.Length) chars, expected at least $minLen). Skipped." + $script:KeysSkipped++ + return + } + $script:NewLines += "`$env:$key = '$current'" + $script:KeysAdded++ + Write-Host "[ready] $key (will write current value to profile)" + } else { + Write-Host "[skipped] $key (kept ephemeral)" + $script:KeysSkipped++ + } + return + } + + Write-Host "" + Write-Host $key + Write-Host " $desc" + if ($url) { Write-Host " Get one at: $url" } + + # AsSecureString = masked input + $secure = Read-Host " Paste key (Enter to skip)" -AsSecureString + $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure) + $value = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) + [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) + + if ([string]::IsNullOrEmpty($value)) { + Write-Host "[skipped] $key" + $script:KeysSkipped++ + return + } + + if ($value -match "'") { + Write-Host "[rejected] $key contains a single quote, which would break the export. Skipped." + $script:KeysSkipped++ + return + } + + if ($value.Length -lt $minLen) { + Write-Host "[rejected] $key looks too short ($($value.Length) chars, expected at least $minLen). Skipped." + $script:KeysSkipped++ + return + } + + # Single-quoted PowerShell strings are literal (no expansion) + $script:NewLines += "`$env:$key = '$value'" + $script:KeysAdded++ + Write-Host "[ready] $key (will write to profile)" +} + +# --- Tier 1 --- + +Write-Host "=== Tier 1 (high-value) ===" + +Prompt-Key 'SEATS_AERO_API_KEY' ` + 'Award flight search across 27 mileage programs. The main event.' ` + 'https://seats.aero/profile (Pro ~$8/mo)' + +Prompt-Key 'DUFFEL_API_KEY_LIVE' ` + 'Real GDS cash flight prices. Free to search, pay per booking.' ` + 'https://duffel.com (use the LIVE key, not test)' + +Prompt-Key 'IGNAV_API_KEY' ` + 'Backup cash flight prices. Fast REST API.' ` + 'https://ignav.com (1,000 free requests/month)' + +Prompt-Key 'AWARDWALLET_API_KEY' ` + 'Auto-pull your loyalty balances, elite status, transfer ratios.' ` + 'https://business.awardwallet.com/profile/api (Business account required)' + +Prompt-Key 'AWARDWALLET_USER_ID' ` + 'Your AwardWallet user ID (paired with the API key above).' ` + '' ` + 3 + +# --- Tier 2 --- + +Write-Host "" +$more = Read-Host "Continue with Tier 2 (SerpAPI, RapidAPI, LiteAPI, TripAdvisor)? [y/N]" +if ($more -match '^[Yy]') { + Prompt-Key 'SERPAPI_API_KEY' 'Google Flights/Hotels comparison data.' 'https://serpapi.com (100 searches/mo free)' + Prompt-Key 'RAPIDAPI_KEY' 'Booking.com Live + Google Flights Live as fallback sources.' 'https://rapidapi.com' + Prompt-Key 'LITEAPI_API_KEY' 'Hotel rate inventory via LiteAPI MCP.' 'https://liteapi.travel' + Prompt-Key 'TRIPADVISOR_API_KEY' 'Hotel ratings, reviews, and rankings.' 'https://tripadvisor-content-api.readme.io (5K calls/mo free)' +} + +# --- Tier 3 --- + +Write-Host "" +$more = Read-Host "Continue with Tier 3 (Scandinavia transit)? [y/N]" +if ($more -match '^[Yy]') { + Prompt-Key 'ENTUR_CLIENT_NAME' "Norway transit search. Free, no signup. Format: 'yourcompany-app'." '' 3 + Prompt-Key 'RESROBOT_API_KEY' 'Sweden rail/bus search.' 'https://www.trafiklab.se (30K calls/mo free)' + Prompt-Key 'REJSEPLANEN_API_KEY' 'Denmark rail/bus search.' 'https://help.rejseplanen.dk' +} + +# --- Write --- + +Write-Host "" +Write-Host "Summary: $KeysAdded to add, $KeysSkipped skipped." + +if ($KeysAdded -eq 0) { + Write-Host "Nothing to write. Done." + exit 0 +} + +$confirm = Read-Host "Write $KeysAdded exports to $PROFILE`? [Y/n]" +if ($confirm -match '^[Nn]') { + Write-Host "Aborted. No changes made." + exit 0 +} + +# Backup if profile has content +if ((Get-Item $PROFILE).Length -gt 0) { + $timestamp = Get-Date -Format 'yyyyMMdd-HHmmss' + $backup = "$PROFILE.bak.$timestamp" + Copy-Item $PROFILE $backup + Write-Host "Backup: $backup" +} + +# Append header + lines +$header = @( + '' + "# Added by travel-hacker setup-keys.ps1 on $(Get-Date)" +) +$header + $NewLines | Add-Content $PROFILE + +Write-Host "Wrote $KeysAdded exports to $PROFILE." +Write-Host "" +Write-Host "Run this to load them now (or open a new PowerShell window):" +Write-Host " . `$PROFILE" +Write-Host "" +Write-Host "Then start Claude Code and ask it to plan a trip." diff --git a/scripts/setup-keys.sh b/scripts/setup-keys.sh new file mode 100755 index 0000000..84f2910 --- /dev/null +++ b/scripts/setup-keys.sh @@ -0,0 +1,263 @@ +#!/usr/bin/env bash +# Travel Hacker key setup. Prompts you locally for API keys, validates them, +# and writes exports to your shell rc with a backup. Never echoes values. +# +# Usage: +# bash setup-keys.sh +# +# Or download and run in one shot: +# bash <(curl -fsSL https://raw.githubusercontent.com/borski/travel-hacking-toolkit/main/scripts/setup-keys.sh) +# +# Supports zsh, bash, fish. PowerShell + cmd users: see scripts/setup-keys.ps1. + +set -eu + +# --- Detect shell and rc file --- + +USER_SHELL="${SHELL##*/}" +case "$USER_SHELL" in + zsh) RCFILE="$HOME/.zshrc" ; SYNTAX="bash" ;; + bash) + if [ "$(uname)" = "Darwin" ]; then + RCFILE="$HOME/.bash_profile" + else + RCFILE="$HOME/.bashrc" + fi + SYNTAX="bash" + ;; + fish) RCFILE="$HOME/.config/fish/config.fish" ; SYNTAX="fish" ;; + *) + echo "Unknown shell: $USER_SHELL" + echo "Supported: zsh, bash, fish." + echo "For PowerShell or cmd, use scripts/setup-keys.ps1 instead." + exit 1 + ;; +esac + +echo "Travel Hacker key setup" +echo " Shell: $USER_SHELL" +echo " RC: $RCFILE" +echo "" +echo "I will prompt you for API keys (input is hidden, never echoed)." +echo "Anything you skip stays unset. All keys are optional." +echo "" + +# --- Helpers --- + +KEYS_ADDED=0 +KEYS_SKIPPED=0 +NEW_LINES="" + +# Make sure rc dir exists for fish +mkdir -p "$(dirname "$RCFILE")" +touch "$RCFILE" + +# Idempotency check (per-shell-syntax) +already_set() { + local key="$1" + case "$SYNTAX" in + bash) grep -qE "^[[:space:]]*export[[:space:]]+${key}=" "$RCFILE" ;; + fish) grep -qE "^[[:space:]]*set[[:space:]]+(-gx|-x)[[:space:]]+${key}[[:space:]]" "$RCFILE" ;; + esac +} + +# Build the export line for the appropriate shell +build_line() { + local key="$1" value="$2" + case "$SYNTAX" in + bash) printf "export %s=%s\n" "$key" "'$value'" ;; + fish) printf "set -gx %s %s\n" "$key" "'$value'" ;; + esac +} + +prompt_key() { + local key="$1" desc="$2" url="$3" min_len="${4:-10}" + + if already_set "$key"; then + echo "[exists] $key (already in $RCFILE, skipping)" + KEYS_SKIPPED=$((KEYS_SKIPPED + 1)) + return + fi + + if [ -n "${!key:-}" ]; then + echo "" + echo "$key is already exported in your current shell but not yet in $RCFILE." + printf " Persist the current value to %s? [y/N] " "$RCFILE" + read -r persist + case "$persist" in + [Yy]*) + # Use the value from the current environment without echoing it + local current_value="${!key}" + # Apply the same validation as paste-input: single-quote and min_len + case "$current_value" in + *\'*) + echo "[rejected] $key value contains a single quote. Skipped." + KEYS_SKIPPED=$((KEYS_SKIPPED + 1)) + return + ;; + esac + if [ ${#current_value} -lt $min_len ]; then + echo "[rejected] $key current value is too short (${#current_value} chars, expected at least $min_len). Skipped." + KEYS_SKIPPED=$((KEYS_SKIPPED + 1)) + return + fi + NEW_LINES="${NEW_LINES}$(build_line "$key" "$current_value") +" + KEYS_ADDED=$((KEYS_ADDED + 1)) + echo "[ready] $key (will write current shell value to rc)" + ;; + *) + echo "[skipped] $key (kept ephemeral)" + KEYS_SKIPPED=$((KEYS_SKIPPED + 1)) + ;; + esac + return + fi + + echo "" + echo "$key" + echo " $desc" + if [ -n "$url" ]; then + echo " Get one at: $url" + fi + + # -s: silent (no echo). -r: raw (no backslash escapes). + printf " Paste key (Enter to skip): " + read -rs value + echo "" + + if [ -z "$value" ]; then + echo "[skipped] $key" + KEYS_SKIPPED=$((KEYS_SKIPPED + 1)) + return + fi + + # Reject single quotes (would break our single-quoted export) + case "$value" in + *\'*) + echo "[rejected] $key contains a single quote, which would break the export. Skipped." + KEYS_SKIPPED=$((KEYS_SKIPPED + 1)) + return + ;; + esac + + # Length sanity check (per-key minimum, default 10) + if [ ${#value} -lt $min_len ]; then + echo "[rejected] $key looks too short (${#value} chars, expected at least $min_len). Skipped." + KEYS_SKIPPED=$((KEYS_SKIPPED + 1)) + return + fi + + NEW_LINES="${NEW_LINES}$(build_line "$key" "$value") +" + KEYS_ADDED=$((KEYS_ADDED + 1)) + echo "[ready] $key (will write to rc)" +} + +# --- Walk through the keys --- + +echo "=== Tier 1 (high-value) ===" + +prompt_key SEATS_AERO_API_KEY \ + "Award flight search across 27 mileage programs. The main event." \ + "https://seats.aero/profile (Pro ~\$8/mo)" + +prompt_key DUFFEL_API_KEY_LIVE \ + "Real GDS cash flight prices. Free to search, pay per booking." \ + "https://duffel.com (use the LIVE key, not test)" + +prompt_key IGNAV_API_KEY \ + "Backup cash flight prices. Fast REST API." \ + "https://ignav.com (1,000 free requests/month)" + +prompt_key AWARDWALLET_API_KEY \ + "Auto-pull your loyalty balances, elite status, transfer ratios." \ + "https://business.awardwallet.com/profile/api (Business account required)" + +prompt_key AWARDWALLET_USER_ID \ + "Your AwardWallet user ID (paired with the API key above)." \ + "" \ + 3 + +echo "" +printf "Continue with Tier 2 (SerpAPI, RapidAPI, LiteAPI, TripAdvisor)? [y/N] " +read -r more +case "$more" in + [Yy]*) + prompt_key SERPAPI_API_KEY \ + "Google Flights/Hotels comparison data." \ + "https://serpapi.com (100 searches/mo free)" + prompt_key RAPIDAPI_KEY \ + "Booking.com Live + Google Flights Live as fallback sources." \ + "https://rapidapi.com" + prompt_key LITEAPI_API_KEY \ + "Hotel rate inventory via LiteAPI MCP." \ + "https://liteapi.travel" + prompt_key TRIPADVISOR_API_KEY \ + "Hotel ratings, reviews, and rankings." \ + "https://tripadvisor-content-api.readme.io (5K calls/mo free)" + ;; +esac + +echo "" +printf "Continue with Tier 3 (Scandinavia transit: Entur, ResRobot, Rejseplanen)? [y/N] " +read -r more +case "$more" in + [Yy]*) + prompt_key ENTUR_CLIENT_NAME \ + "Norway transit search. Free, no signup. Format: 'yourcompany-app'." \ + "" \ + 3 + prompt_key RESROBOT_API_KEY \ + "Sweden rail/bus search." \ + "https://www.trafiklab.se (30K calls/mo free)" + prompt_key REJSEPLANEN_API_KEY \ + "Denmark rail/bus search." \ + "https://help.rejseplanen.dk" + ;; +esac + +# --- Write --- + +echo "" +echo "Summary: $KEYS_ADDED to add, $KEYS_SKIPPED skipped." + +if [ "$KEYS_ADDED" -eq 0 ]; then + echo "Nothing to write. Done." + exit 0 +fi + +printf "Write %d exports to %s? [Y/n] " "$KEYS_ADDED" "$RCFILE" +read -r confirm +case "$confirm" in + [Nn]*) + echo "Aborted. No changes made." + exit 0 + ;; +esac + +# Backup if file has content +if [ -s "$RCFILE" ]; then + TIMESTAMP=$(date +%Y%m%d-%H%M%S) + BACKUP="$RCFILE.bak.$TIMESTAMP" + cp "$RCFILE" "$BACKUP" + echo "Backup: $BACKUP" +fi + +# Append a header + the lines +{ + echo "" + echo "# Added by travel-hacker setup-keys.sh on $(date)" + printf "%s" "$NEW_LINES" +} >> "$RCFILE" + +echo "Wrote $KEYS_ADDED exports to $RCFILE." +echo "" +echo "Run this to load them now (or open a new terminal):" +case "$SYNTAX" in + bash) echo " source \"$RCFILE\"" ;; + fish) echo " source \"$RCFILE\"" ;; +esac + +echo "" +echo "Then start Claude Code and ask it to plan a trip." From 551621c2ea936f6c213e1ff01fd342e4b0d6d3d9 Mon Sep 17 00:00:00 2001 From: Michael Borohovski Date: Fri, 1 May 2026 14:25:07 -0700 Subject: [PATCH 5/7] test(plugin): smoke test plugin validation, agent sync, drift detection Adds five new static checks to scripts/smoke-test.sh: 1. setup-keys.sh syntax (bash -n). Catches the curl|bash one-liner breaking silently if a future edit introduces a syntax error. 2. setup-keys.ps1 structure (brace-counting). Same idea for the PowerShell version since we don't have pwsh in the local CI image. 3. Claude plugin manifest + marketplace validation via 'claude plugin validate .'. Skips cleanly if the claude CLI isn't installed. 4. agents/travel-hacker.md is in sync with CLAUDE.md (diff -q) AND has the required frontmatter fields (name, description, model). Guards against contributors editing CLAUDE.md without running sync-agent.sh, and against frontmatter regressions. 5. Plugin component discovery: skills/ non-empty + .mcp.json valid JSON. Catches the case where a directory was deleted or a config corrupted. Also refactors the agent invocation test for the Claude path. Was calling claude --strict-mcp-config --mcp-config .mcp.json -p; now uses claude --plugin-dir 'REPO_DIR' -p. The plugin loads skills + MCPs + agent system prompt atomically so the old MCP-only flag is obsolete. The test_agent function is refactored to take an explicit binary parameter so 'claude' and 'claude-plugin' aren't conflated. Docker image manifest check now distinguishes network/auth errors from genuinely missing images. If GHCR is unreachable from the runtime environment (DNS, network, credentials), the test skips with a clear message. Restricted CI environments no longer false-fail this check. Updated header comment to enumerate the full check set (12 static + 3 agent invocations x 2 each = 18 total when all CLIs are installed). --- scripts/smoke-test.sh | 153 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 133 insertions(+), 20 deletions(-) diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index 4768c46..4a61d91 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -3,12 +3,20 @@ # Smoke test the toolkit across all three supported AI coding tools. # Run this after any change to skills, CLAUDE.md, or MCP config. # -# What it checks: -# 1. setup.sh and setup.ps1 syntax parse cleanly +# What it checks (static): +# 1. setup.sh, setup-keys.sh, setup.ps1, setup-keys.ps1 syntax/structure # 2. Every skill has valid frontmatter (name + description) # 3. CLAUDE.md is under the 40k threshold that triggers Claude Code's warning -# 4. Each agent (codex, claude, opencode) starts cleanly from the toolkit -# 5. Each agent picks reasonable skills for a real travel question +# 4. All Docker images exist on ghcr.io (and are pullable without auth) +# 5. Data files within their declared TTL +# 6. README.md and llms.txt match the auto-generated tables (no drift) +# 7. Claude plugin manifest + marketplace.json validate via `claude plugin validate` +# 8. agents/travel-hacker.md is in sync with CLAUDE.md and has required frontmatter +# 9. Plugin components are present (skills/, .mcp.json valid JSON) +# +# What it checks (agent invocations, slower): +# - Each agent (codex, claude, opencode) starts cleanly from the toolkit +# - Each agent picks reasonable skills for a real travel question # # Usage: # bash scripts/smoke-test.sh # run all checks @@ -52,6 +60,13 @@ if [ "$AGENTS_ONLY" -eq 0 ]; then fail "scripts/setup.sh syntax error" fi + # 1b. setup-keys.sh syntax (security-sensitive, used by curl|bash one-liner) + if bash -n scripts/setup-keys.sh 2>/dev/null; then + ok "scripts/setup-keys.sh syntax" + else + fail "scripts/setup-keys.sh syntax error" + fi + # 2. setup.ps1 structure (we don't have pwsh here, but we can sanity-check braces and here-strings) if python3 - <<'PY' 2>/dev/null import sys @@ -70,6 +85,21 @@ PY fail "scripts/setup.ps1 structure" fi + # 2b. setup-keys.ps1 structure (parse-level brace check) + if python3 - <<'PY' 2>/dev/null +import sys +with open('scripts/setup-keys.ps1') as f: + c = f.read() +opens, closes = c.count('{'), c.count('}') +if opens != closes: + sys.exit(f'brace mismatch ({opens} vs {closes})') +PY + then + ok "scripts/setup-keys.ps1 structure" + else + fail "scripts/setup-keys.ps1 structure" + fi + # 3. Skill frontmatter bad_skills=0 for f in skills/*/SKILL.md; do @@ -97,17 +127,35 @@ PY # 5. Docker images exist on ghcr (manifest inspect needs no pull) if command -v docker >/dev/null 2>&1; then - bad_images=0 - for image in patchright-docker sw-fares aa-miles-check chase-travel amex-travel ticketsatwork; do - if ! docker manifest inspect "ghcr.io/borski/$image:latest" >/dev/null 2>&1; then - echo " missing manifest: ghcr.io/borski/$image:latest" - bad_images=$((bad_images+1)) - fi - done - if [ "$bad_images" -eq 0 ]; then - ok "all 6 Docker images exist on ghcr.io" + # Probe whether GHCR is reachable at all. If not, skip rather than report + # all images as missing (DNS/network/auth failures look identical to + # genuinely missing images otherwise). + probe_out=$(docker manifest inspect "ghcr.io/borski/patchright-docker:latest" 2>&1 >/dev/null) || true + if echo "$probe_out" | grep -qE "no such host|connection refused|i/o timeout|TLS handshake|x509|dial tcp"; then + skip "ghcr.io unreachable from this environment, skipping image check (network/DNS issue, not image issue): $(echo "$probe_out" | head -1)" + elif echo "$probe_out" | grep -qE "unauthorized|access denied|denied: requested access"; then + skip "ghcr.io authentication issue, skipping image check (not an image issue): $(echo "$probe_out" | head -1)" else - fail "$bad_images Docker images missing on ghcr.io" + bad_images=0 + missing_list="" + for image in patchright-docker sw-fares aa-miles-check chase-travel amex-travel ticketsatwork; do + out=$(docker manifest inspect "ghcr.io/borski/$image:latest" 2>&1 >/dev/null) || true + if [ -n "$out" ]; then + # Re-classify network/auth errors so we don't fail the test for them + if echo "$out" | grep -qE "no such host|connection refused|i/o timeout|TLS handshake|x509|dial tcp|unauthorized|access denied|denied: requested access"; then + echo " transient (network/auth) for ghcr.io/borski/$image:latest, skipping" + else + echo " missing manifest: ghcr.io/borski/$image:latest ($(echo "$out" | head -1))" + bad_images=$((bad_images+1)) + missing_list="$missing_list $image" + fi + fi + done + if [ "$bad_images" -eq 0 ]; then + ok "all 6 Docker images exist on ghcr.io" + else + fail "$bad_images Docker image(s) genuinely missing on ghcr.io:$missing_list" + fi fi else skip "docker not installed (skipping image manifest check)" @@ -128,6 +176,70 @@ PY fail "README.md or llms.txt drifted from generated tables (run: bash scripts/gen-skill-tables.sh)" sed 's/^/ /' /tmp/gendrift.out | head -30 fi + + # 8. Claude plugin manifest + marketplace validate + if [ -f .claude-plugin/plugin.json ] && [ -f .claude-plugin/marketplace.json ]; then + if command -v claude >/dev/null 2>&1; then + # Run with a hard timeout in case the CLI hangs (e.g., interactive auth + # prompt in a CI environment without credentials). Exit code 124 = timeout. + if timeout 30 claude plugin validate . >/tmp/plugin-validate.out 2>&1; then + ok "Claude plugin + marketplace manifests valid" + else + rc=$? + if [ "$rc" -eq 124 ]; then + skip "claude plugin validate timed out (likely an auth prompt in non-interactive CI)" + elif grep -qiE "log in|please run.*login|not logged in|unauthorized|api key" /tmp/plugin-validate.out; then + skip "claude plugin validate needs auth (skipping in this environment)" + else + fail "Claude plugin or marketplace validation failed" + sed 's/^/ /' /tmp/plugin-validate.out | head -10 + fi + fi + else + skip "claude CLI not installed (skipping plugin validation)" + fi + else + fail "missing .claude-plugin/plugin.json or .claude-plugin/marketplace.json" + fi + + # 9. Agent file in sync with CLAUDE.md, with required frontmatter + if [ -f agents/travel-hacker.md ]; then + if diff -q CLAUDE.md agents/travel-hacker.md >/dev/null 2>&1; then + # Verify required frontmatter fields + missing_fields=() + for field in name description model; do + if ! awk '/^---$/{c++; next} c==1' agents/travel-hacker.md | grep -qE "^${field}:[[:space:]]"; then + missing_fields+=("$field") + fi + done + if [ "${#missing_fields[@]}" -eq 0 ]; then + ok "agents/travel-hacker.md in sync with CLAUDE.md and has required frontmatter" + else + fail "agents/travel-hacker.md is missing required frontmatter fields: ${missing_fields[*]}" + fi + else + fail "agents/travel-hacker.md drifted from CLAUDE.md (run: bash scripts/sync-agent.sh)" + fi + else + fail "agents/travel-hacker.md missing (run: bash scripts/sync-agent.sh)" + fi + + # 10. Plugin component discovery: skills/, .mcp.json present and parseable + component_errors=0 + if [ ! -d skills ] || [ -z "$(ls -A skills 2>/dev/null)" ]; then + fail "plugin: skills/ directory missing or empty" + component_errors=$((component_errors+1)) + fi + if [ ! -f .mcp.json ]; then + fail "plugin: .mcp.json missing" + component_errors=$((component_errors+1)) + elif ! python3 -c "import json; json.load(open('.mcp.json'))" 2>/dev/null; then + fail "plugin: .mcp.json is not valid JSON" + component_errors=$((component_errors+1)) + fi + if [ "$component_errors" -eq 0 ]; then + ok "plugin: components present (skills/, .mcp.json, agents/travel-hacker.md)" + fi fi # --- Agent invocations --- @@ -145,10 +257,11 @@ else test_agent() { local name="$1" - shift + local binary="$2" + shift 2 local -a cmd=("$@") - if ! command -v "$name" >/dev/null 2>&1; then - skip "$name: not installed (skipping)" + if ! command -v "$binary" >/dev/null 2>&1; then + skip "$name: $binary not installed (skipping)" return fi @@ -197,9 +310,9 @@ else fi } - test_agent codex codex exec - test_agent claude claude --strict-mcp-config --mcp-config .mcp.json -p - test_agent opencode opencode run + test_agent codex codex codex exec + test_agent claude claude claude --plugin-dir "$REPO_DIR" -p + test_agent opencode opencode opencode run fi echo "" From 821d53b4ccbac25699b0f3412f21cad35312a761 Mon Sep 17 00:00:00 2001 From: Michael Borohovski Date: Fri, 1 May 2026 14:25:22 -0700 Subject: [PATCH 6/7] chore(plugin): align setup scripts with plugin-first install path Five changes to setup.sh, setup.ps1, and .gitignore, plus a deletion: 1. Drop the .claude/settings.local.json copy step. Anthropic's plugin userConfig was supposed to handle Claude Code key management but is broken upstream (anthropics/claude-code#11927). The settings.local.json path was the workaround pre-PR; now the canonical path is shell env vars set via scripts/setup-keys.sh. The example file is removed too. 2. Tell Claude users where to actually configure keys: shell rc + the guided /travel-hacker:getting-started skill. Replaces the obsolete 'edit .claude/settings.local.json' instruction. 3. Show install-hooks.sh output instead of redirecting to /dev/null. When the script skips a foreign pre-commit hook, the contributor sees the message instead of silently losing sync enforcement. 4. Recommend 'claude --plugin-dir .' instead of the old '--strict-mcp-config --mcp-config .mcp.json' as the launch command from inside a clone. The plugin loads skills + MCPs + agent system prompt atomically, so the old MCP-only flag pair is obsolete. 5. .gitignore: also ignore .env.backup* and .env.bak so timestamped backups from local key rotations don't accidentally get staged. --- .claude/settings.local.json.example | 13 ------------- .gitignore | 2 ++ scripts/setup.ps1 | 18 +++++------------- scripts/setup.sh | 22 ++++++++++------------ 4 files changed, 17 insertions(+), 38 deletions(-) delete mode 100644 .claude/settings.local.json.example diff --git a/.claude/settings.local.json.example b/.claude/settings.local.json.example deleted file mode 100644 index 5bbcf5e..0000000 --- a/.claude/settings.local.json.example +++ /dev/null @@ -1,13 +0,0 @@ -{ - "env": { - "SEATS_AERO_API_KEY": "", - "SERPAPI_API_KEY": "", - "RAPIDAPI_KEY": "", - "AWARDWALLET_API_KEY": "", - "AWARDWALLET_USER_ID": "", - "DUFFEL_API_KEY_LIVE": "", - "LITEAPI_API_KEY": "", - "ENTUR_CLIENT_NAME": "", - "RESROBOT_API_KEY": "" - } -} diff --git a/.gitignore b/.gitignore index 639a103..7b07459 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # API keys .env +.env.backup* +.env.bak .claude/settings.local.json # Dependencies diff --git a/scripts/setup.ps1 b/scripts/setup.ps1 index 00c6fa5..a63c68a 100644 --- a/scripts/setup.ps1 +++ b/scripts/setup.ps1 @@ -167,17 +167,8 @@ function Setup-ApiKeys { } if ($UseClaude) { - $claudeSettings = Join-Path $RepoDir '.claude\settings.local.json' - $claudeExample = Join-Path $RepoDir '.claude\settings.local.json.example' - if (-not (Test-Path $claudeSettings)) { - if (Test-Path $claudeExample) { - Copy-Item -LiteralPath $claudeExample -Destination $claudeSettings - Write-Host " Created .claude\settings.local.json (Claude Code, auto-gitignored)." - Write-Host " Edit it to add your API keys." - } - } else { - Write-Host " .claude\settings.local.json already exists. Skipping." - } + Write-Host " Claude Code reads API keys from your PowerShell profile environment, not from a config file." + Write-Host " Use scripts\setup-keys.ps1 after this finishes (or run /travel-hacker:getting-started inside Claude Code)." } Write-Host "" @@ -485,7 +476,7 @@ if ($UseOpenCode) { Write-Host " OpenCode: opencode" } if ($UseClaude) { - Write-Host " Claude Code: claude --strict-mcp-config --mcp-config .mcp.json" + Write-Host " Claude Code: claude --plugin-dir ." } if ($UseCodex) { Write-Host " Codex: codex" @@ -497,7 +488,8 @@ if ($UseOpenCode -or $UseCodex) { Write-Host "Add your API keys: edit .env" } if ($UseClaude) { - Write-Host "Add your API keys: edit .claude\settings.local.json" + Write-Host "Add your API keys: set them in your PowerShell profile (`$PROFILE)," + Write-Host " or run /travel-hacker:getting-started inside Claude Code." } Write-Host "" diff --git a/scripts/setup.sh b/scripts/setup.sh index 31fc23a..4de963f 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -63,16 +63,8 @@ setup_api_keys() { fi if [ "$USE_CLAUDE" -eq 1 ]; then - local claude_settings="$REPO_DIR/.claude/settings.local.json" - if [ ! -f "$claude_settings" ]; then - if [ -f "$REPO_DIR/.claude/settings.local.json.example" ]; then - cp "$REPO_DIR/.claude/settings.local.json.example" "$claude_settings" - echo " Created .claude/settings.local.json (Claude Code, auto-gitignored)." - echo " Edit it to add your API keys." - fi - else - echo " .claude/settings.local.json already exists. Skipping." - fi + echo " Claude Code reads API keys from your shell environment, not from a config file." + echo " Use scripts/setup-keys.sh after this finishes (or run /travel-hacker:getting-started inside Claude Code)." fi echo "" @@ -278,6 +270,11 @@ setup_api_keys install_atlas_deps install_optional_tools +# Install git hooks for contributors. Safe to run on any clone; no-ops outside git. +echo "" +echo "Installing git hooks..." +bash "$REPO_DIR/scripts/install-hooks.sh" 2>&1 | sed 's/^/ /' || true + if [ "$USE_CODEX" -eq 1 ]; then install_codex_plugin fi @@ -295,7 +292,7 @@ if [ "$USE_OPENCODE" -eq 1 ]; then echo " OpenCode: opencode" fi if [ "$USE_CLAUDE" -eq 1 ]; then - echo " Claude Code: claude --strict-mcp-config --mcp-config .mcp.json" + echo " Claude Code: claude --plugin-dir ." fi if [ "$USE_CODEX" -eq 1 ]; then echo " Codex: source .env && codex" @@ -307,7 +304,8 @@ if [ "$USE_OPENCODE" -eq 1 ] || [ "$USE_CODEX" -eq 1 ]; then echo "Add your API keys: edit .env" fi if [ "$USE_CLAUDE" -eq 1 ]; then - echo "Add your API keys: edit .claude/settings.local.json" + echo "Add your API keys: set them in your shell rc (~/.zshrc or ~/.bashrc)," + echo " or run /travel-hacker:getting-started inside Claude Code." fi echo "" From 524820d9faf56c7b2cde9d9650ed480ea0e1aff8 Mon Sep 17 00:00:00 2001 From: Michael Borohovski Date: Fri, 1 May 2026 14:25:45 -0700 Subject: [PATCH 7/7] docs(plugin): rewrite quick start, document install paths, fix project tree README quick-start rewritten around the no-clone install paths: - Claude Code: /plugin marketplace add + /plugin install. Two commands. After install, run /travel-hacker:getting-started for guided key setup. - Codex: codex plugin marketplace add. One command. Same marketplace catalog, different plugin format inside the repo (.codex-plugin vs .claude-plugin). - Cowork (Claude Desktop's agent panel): no /plugin slash commands of its own, but inherits anything installed via Claude Code from ~/.claude/plugins/. Documents the indirection clearly. - Clone path preserved for OpenCode users (no plugin format) and for contributors. claude --plugin-dir . loads skills + MCPs + agent atomically. API key setup section now leads with scripts/setup-keys.sh and .ps1. Manual setup with shell rc edits is preserved as a sub-heading. Includes a 1Password 'op run --env-file=.env -- claude' option. Notes the upstream userConfig bugs (anthropics/claude-code#11927, #39125) so readers understand why the toolkit reads from shell env vars instead of plugin settings. First-run verification: /travel-hacker:getting-started or shell-side 'claude plugin list | grep travel-hacker'. Project structure tree updated: adds .claude-plugin/, agents/, .github/workflows/, scripts/setup-keys.{sh,ps1}, scripts/sync-agent.sh, scripts/install-hooks.sh, scripts/hooks/pre-commit, plus the two new orchestration skills (getting-started, plan-trip). Smoke test description ('What it verifies') now lists all 12 static checks instead of the obsolete 8. Pre-existing miscount fixed: 'Six Docker-based skills' is actually five skills plus a shared base image. Both occurrences in the README updated. Description tweaks: '6 MCP servers (5 free + LiteAPI which needs a key)' clarifies what 'free' meant in the next paragraph. README claim that the script 'rejects values with shell metacharacters' was an overstatement; reworded to match what the validation actually does (reject single quotes + min length check, plus single-quoted exports defuse other metacharacters). llms.txt regenerated via gen-skill-tables.sh to match. --- README.md | 187 ++++++++++++++++++++++++++++++++++++++++++++---------- llms.txt | 2 + 2 files changed, 155 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 7687ce1..dc723cf 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,110 @@ AI-powered travel hacking with points, miles, and award flights. Drop-in skills and MCP servers for [OpenCode](https://opencode.ai), [Claude Code](https://docs.anthropic.com/en/docs/claude-code), and [Codex](https://openai.com/codex/). -Ask your AI to find you a 60,000-mile business class flight to Tokyo. It'll search award availability across 25+ programs, compare against cash prices, check your loyalty balances, and tell you the best play. +Ask your AI to find you a 60,000-mile business class flight to Tokyo. It'll search award availability across 27 mileage programs, compare against cash prices, check your loyalty balances, and tell you the best play. ## Quick Start +### Install via plugin (no clone) + +The easiest way to use the toolkit. Pick your tool: + +#### Claude Code + +Inside Claude Code, run: + +``` +/plugin marketplace add borski/travel-hacking-toolkit +/plugin install travel-hacker@borski-travel +``` + +Done. 42 skills, 6 MCP servers (5 free + LiteAPI which needs a key), and the `travel-hacker` subagent are installed. Run `claude` from anywhere and ask it to plan a trip. + +To verify the install, run `/travel-hacker:getting-started` inside Claude Code. It tells you which API keys are configured and points at the local setup script for the missing ones. Or in your shell: `claude plugin list | grep travel-hacker` should show the plugin. + +#### Codex + +In your terminal: + +```bash +codex plugin marketplace add borski/travel-hacking-toolkit +``` + +Then start Codex and the plugin appears in `/plugins`. Codex pulls the marketplace catalog from the repo (`.agents/plugins/marketplace.json`) and the plugin manifest from `plugins/travel-hacking-toolkit/.codex-plugin/plugin.json`, so the install is one command. + +#### Cowork (Claude Desktop's agent panel) + +Cowork itself doesn't expose `/plugin` slash commands, so you can't install plugins from inside a Cowork chat. Install via Claude Code first; Cowork picks it up automatically because both share `~/.claude/plugins/`. + +1. **In your terminal**, run `claude` +2. **In Claude Code**, run the two install commands from the [Claude Code section above](#claude-code) +3. **Open Cowork** inside Claude Desktop. The plugin is already there. + +Claude Desktop's chat UI doesn't support the plugin format yet, so use Cowork inside the same app for the full experience. + +### Configure API keys + +The 5 free MCP servers (Skiplagged, Kiwi, Trivago, Ferryhopper, Airbnb) work immediately with zero keys. Cash flight and hotel search work out of the box. + +To unlock award search and the rest of the toolkit, you need API keys set as environment variables in your shell. Run the local setup script: + +**macOS / Linux / WSL / Git Bash:** + +```bash +bash <(curl -fsSL https://raw.githubusercontent.com/borski/travel-hacking-toolkit/main/scripts/setup-keys.sh) +``` + +**Windows (PowerShell):** + +```powershell +iwr https://raw.githubusercontent.com/borski/travel-hacking-toolkit/main/scripts/setup-keys.ps1 -OutFile $env:TEMP\setup-keys.ps1 +powershell -NoProfile -ExecutionPolicy Bypass -File $env:TEMP\setup-keys.ps1 +Remove-Item $env:TEMP\setup-keys.ps1 +``` + +The script prompts for each key with masked input, validates them (rejects values with single quotes that would break the export, plus a per-key minimum length sanity check), writes them to the right shell rc with a backup, and never echoes the values back. The exports are single-quoted so other shell metacharacters in the key are safe. Keys never enter your terminal scrollback or any chat session. The `/travel-hacker:getting-started` skill inside Claude Code points at the same script. + +The 4 highest-value keys: + +| Key | What it unlocks | Cost | +|-----|------|-----------| +| `SEATS_AERO_API_KEY` | Award flight search across 27 mileage programs. The main event. | Pro ~$8/mo | +| `DUFFEL_API_KEY_LIVE` | Real GDS cash flight prices. | Free to search, pay per booking | +| `IGNAV_API_KEY` | Backup cash flight prices. Fast REST API. | 1,000 free requests/month | +| `AWARDWALLET_API_KEY` + `AWARDWALLET_USER_ID` | Auto-pull your loyalty balances, elite status, transfer ratios. | Business account required | + +Other keys (SerpAPI, RapidAPI, LiteAPI, TripAdvisor, RESROBOT, Rejseplanen, Entur) extend specific skills. The setup skill covers them too. See the [full API key reference](#api-keys--signup-links) for signup links. + +Five Docker-based skills (Southwest, American Airlines, Chase, Amex, TicketsAtWork) handle sites that don't have public APIs, plus a shared base image. They auto-pull on first use. See the [Docker Images](#docker-images) section for credentials and configuration. + +#### Manual setup (if you'd rather not use the skill) + +Add to your shell rc (`~/.zshrc`, `~/.bashrc`, etc.): + +```bash +export SEATS_AERO_API_KEY="your-key-here" +export DUFFEL_API_KEY_LIVE="your-key-here" +export IGNAV_API_KEY="your-key-here" +export AWARDWALLET_API_KEY="your-key-here" +export AWARDWALLET_USER_ID="your-user-id" +``` + +Reload (`source ~/.zshrc`) or open a new terminal, then run `claude`. + +If you keep secrets in 1Password, you can resolve at launch instead: + +```bash +op run --env-file=.env -- claude +``` + +(See [1Password CLI docs](https://developer.1password.com/docs/cli/secrets-environment-variables/) for the `.env` template syntax.) + +> The toolkit reads keys from the shell environment because Anthropic's plugin `userConfig` mechanism is currently broken for bash-based skills ([anthropics/claude-code#11927](https://github.com/anthropics/claude-code/issues/11927), [#39125](https://github.com/anthropics/claude-code/issues/39125)). When that's fixed upstream, we'll move to the cleaner in-plugin config flow. + +### Clone path (for OpenCode users or contributors) + +OpenCode doesn't have a plugin format, so OpenCode users clone the repo. Contributors who want to hack on the toolkit also clone. + ```bash git clone https://github.com/borski/travel-hacking-toolkit.git cd travel-hacking-toolkit @@ -17,36 +117,24 @@ cd travel-hacking-toolkit .\scripts\setup.cmd ``` -The setup script walks you through everything: picks your tool (OpenCode, Claude Code, Codex, or all three), creates your API key config files, installs dependencies, installs the Codex plugin when selected, and optionally installs skills system-wide for OpenCode and Claude Code. - -On Windows the `.cmd` wrapper launches `scripts\setup.ps1` with an ExecutionPolicy bypass so nothing needs to be unblocked first. You can also run the PowerShell script directly: `powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\setup.ps1`. +The setup script picks your tool (OpenCode, Claude Code, Codex, or all three), creates your API key config file, installs dependencies, and optionally installs skills system-wide for OpenCode and Claude Code. -The 5 free MCP servers (Skiplagged, Kiwi, Trivago, Ferryhopper, Airbnb) work immediately with zero API keys. For the full experience, add at minimum: +On Windows the `.cmd` wrapper launches `scripts\setup.ps1` with an ExecutionPolicy bypass so nothing needs to be unblocked first. -| Key | Why | Free Tier | -|-----|-----|-----------| -| `SEATS_AERO_API_KEY` | Award flight search. The main event. | No (Pro ~$8/mo) | -| `DUFFEL_API_KEY_LIVE` | Primary cash flight prices. Real GDS data. | Yes (search free, pay per booking) | -| `IGNAV_API_KEY` | Secondary cash flight prices. Fast REST API. | Yes (1,000 free requests) | - -Six skills run as optional Docker containers (Southwest, American Airlines, Chase, Amex, TicketsAtWork, plus a shared base image). `setup.sh` auto-pulls them when you select the relevant tools. See the [Docker Images](#docker-images) section for the full catalog and usage examples. - -Then launch your tool: +Then launch your tool from inside the repo: ```bash # OpenCode opencode -# Claude Code -claude --strict-mcp-config --mcp-config .mcp.json +# Claude Code (loads the plugin from the working tree, no install needed) +claude --plugin-dir . # Codex source .env && codex ``` -The `--strict-mcp-config` flag tells Claude Code to load MCP servers from the config file directly. This is more reliable than auto-discovery ([known issue](https://github.com/anthropics/claude-code/issues/5037)). - -For Codex, `./scripts/setup.sh` installs a local plugin symlink at `~/.codex/plugins/travel-hacking-toolkit` and adds a marketplace entry at `~/.agents/plugins/marketplace.json`. The plugin exposes the repo's `skills/` directory and the travel MCP servers as one installable bundle. +`claude --plugin-dir .` loads the plugin (skills, MCP servers, and the `travel-hacker` subagent) directly from your clone. Changes you make to the toolkit are picked up immediately. Useful when you're contributing. ## What's Included @@ -75,6 +163,8 @@ Start here: the **orchestration skills** call everything else automatically. | **award-calendar** | Cheapest award dates for a route across a date range. Calendar grid view. | Seats.aero Pro | | **compare-flights** | Unified flight comparison across ALL sources in parallel. Auto-applies transfer optimization. | Uses individual skill keys | | **compare-hotels** | Unified hotel comparison across portals, metasearch, and Airbnb. FHR/Edit stacking detection. | Uses individual skill keys | +| **getting-started** | First-run onboarding. Detects setup, points to setup-keys script, shows sample prompts. | None | +| **plan-trip** | Guided trip planner. The hero command for the toolkit. | None | | **trip-calculator** | Cash vs points decision answered with math. Transfer ratios, taxes, opportunity cost. | None (free, local data) | | **trip-planner** | Full trip planning. Flights + hotels + points in one shot. | Uses individual skill keys | @@ -181,7 +271,7 @@ These skills require an external account or API key signup. See [`.env.example`] ## Docker Images -Six skills run as Docker containers (browser-automated via Patchright). All images are public on GitHub Container Registry, no auth required to pull. `setup.sh` (and `setup.ps1` on Windows) auto-pulls the ones you need based on which tool you select. +Five skills run as Docker containers (browser-automated via Patchright), plus a shared `patchright-docker` base image they all build on. All six images are public on GitHub Container Registry, no auth required to pull. `setup.sh` (and `setup.ps1` on Windows) auto-pulls the ones you need based on which tool you select. | Image | Skill | Purpose | Source | @@ -341,19 +431,25 @@ The core question: **"Should I burn points or pay cash?"** ``` travel-hacking-toolkit/ +├── .claude-plugin/ +│ ├── plugin.json # Claude Code plugin manifest (this is the install root) +│ └── marketplace.json # Claude Code marketplace catalog ├── .agents/ -│ ├── plugins/marketplace.json # Repo-local Codex marketplace entry +│ ├── plugins/marketplace.json # Codex marketplace catalog │ └── skills -> ../skills # Codex auto-discovery (no plugin install needed) ├── AGENTS.md -> CLAUDE.md # OpenCode project instructions (symlink) -├── CLAUDE.md # Project instructions and workflow guidance +├── CLAUDE.md # Project instructions, agent system prompt (frontmatter at top) +├── agents/ +│ └── travel-hacker.md # Plugin agent file (auto-synced from CLAUDE.md) ├── opencode.json # OpenCode MCP server config ├── .mcp.json # Claude Code MCP server config -├── .env.example # API key template (OpenCode/Codex) +├── .env.example # API key template (OpenCode/Codex clone path) ├── .claude/ -│ ├── settings.local.json.example # API key template (Claude Code) │ └── skills -> ../skills # Symlink to skills ├── .opencode/ │ └── skills -> ../skills # Symlink to skills +├── .github/workflows/ +│ └── smoke-test.yml # CI: runs scripts/smoke-test.sh --quick on PRs ├── plugins/ │ └── travel-hacking-toolkit/ │ ├── .codex-plugin/plugin.json # Codex plugin manifest @@ -372,6 +468,8 @@ travel-hacking-toolkit/ │ ├── award-calendar/SKILL.md # Cheapest award dates across a date range │ ├── compare-flights/SKILL.md # Unified flight comparison (all sources) │ ├── compare-hotels/SKILL.md # Unified hotel comparison (all sources) +│ ├── getting-started/SKILL.md # First-run setup detector + signpost to setup-keys +│ ├── plan-trip/SKILL.md # User-invoked guided trip planner (the hero command) │ ├── trip-calculator/SKILL.md # Cash vs points calculator │ ├── trip-planner/SKILL.md # Full trip planning in one shot │ │ @@ -448,7 +546,16 @@ travel-hacking-toolkit/ ├── scripts/ │ ├── setup.sh # Interactive installer (macOS/Linux/WSL/Git Bash) │ ├── setup.ps1 # Interactive installer (Windows PowerShell) -│ └── setup.cmd # Windows launcher (invokes setup.ps1) +│ ├── setup.cmd # Windows launcher (invokes setup.ps1) +│ ├── setup-keys.sh # Standalone API key setup (no clone needed; curl|bash works) +│ ├── setup-keys.ps1 # Standalone API key setup (PowerShell) +│ ├── sync-agent.sh # Copies CLAUDE.md to agents/travel-hacker.md (Claude plugin) +│ ├── install-hooks.sh # Installs git hooks from scripts/hooks/ (run by setup.sh) +│ ├── smoke-test.sh # Static + agent integration tests +│ ├── gen-skill-tables.sh # Regenerates skill tables in README.md and llms.txt +│ ├── check-data-freshness.sh # Validates data/*.json TTLs +│ └── hooks/ +│ └── pre-commit # Auto-syncs agents/travel-hacker.md when CLAUDE.md is staged └── LICENSE # MIT ``` @@ -462,15 +569,27 @@ bash scripts/smoke-test.sh --quick # static checks only (no agent invocations) bash scripts/smoke-test.sh --agents # agent invocations only ``` -What it verifies: -1. `setup.sh` and `setup.ps1` syntax parse cleanly -2. Every skill has valid `name` + `description` frontmatter -3. CLAUDE.md is under Claude Code's 40k char warning threshold -4. All Docker images exist on ghcr.io -5. All data files are within their declared TTL -6. README.md and llms.txt skill tables match the generated output (no drift) -7. Each agent (codex, claude, opencode) starts cleanly from the toolkit -8. Each agent picks the right skills (`lessons-learned` + `flight-search-strategy` minimum) for a real travel question +What it verifies (12 static checks + 2 agent invocations per agent): + +Static (12 checks): + +1. `setup.sh` bash syntax +2. `setup-keys.sh` bash syntax +3. `setup.ps1` structural integrity (braces, here-strings) +4. `setup-keys.ps1` structural integrity (braces) +5. Every skill has valid `name` + `description` frontmatter +6. CLAUDE.md is under Claude Code's 40k char warning threshold +7. All 6 Docker images exist on ghcr.io (skipped cleanly if the registry is unreachable, never reported as missing) +8. All data files are within their declared TTL +9. README.md and llms.txt skill tables match the generated output (no drift) +10. Claude plugin manifest + marketplace.json validate via `claude plugin validate` +11. `agents/travel-hacker.md` is in sync with CLAUDE.md and has required frontmatter (`name`, `description`, `model`) +12. Plugin component discovery: `skills/` non-empty, `.mcp.json` valid JSON + +Agent invocations (per supported agent: codex, claude, opencode): + +- Startup: agent loads cleanly from the toolkit +- Skill discovery: agent picks the right skills (`lessons-learned` + `flight-search-strategy` minimum) for a real travel question Missing CLIs are skipped, not failed. Run from the repo root. diff --git a/llms.txt b/llms.txt index b910002..2816b58 100644 --- a/llms.txt +++ b/llms.txt @@ -18,6 +18,8 @@ Skill listings below are auto-generated from `skills/*/SKILL.md` frontmatter by - [Award Calendar](https://github.com/borski/travel-hacking-toolkit/blob/main/skills/award-calendar/SKILL.md): Cheapest award dates for a route across a date range. Calendar grid view. - [Compare Flights](https://github.com/borski/travel-hacking-toolkit/blob/main/skills/compare-flights/SKILL.md): Unified flight comparison across ALL sources in parallel. Auto-applies transfer optimization. - [Compare Hotels](https://github.com/borski/travel-hacking-toolkit/blob/main/skills/compare-hotels/SKILL.md): Unified hotel comparison across portals, metasearch, and Airbnb. FHR/Edit stacking detection. +- [Getting Started](https://github.com/borski/travel-hacking-toolkit/blob/main/skills/getting-started/SKILL.md): First-run onboarding. Detects setup, points to setup-keys script, shows sample prompts. +- [Plan Trip](https://github.com/borski/travel-hacking-toolkit/blob/main/skills/plan-trip/SKILL.md): Guided trip planner. The hero command for the toolkit. - [Trip Calculator](https://github.com/borski/travel-hacking-toolkit/blob/main/skills/trip-calculator/SKILL.md): Cash vs points decision answered with math. Transfer ratios, taxes, opportunity cost. - [Trip Planner](https://github.com/borski/travel-hacking-toolkit/blob/main/skills/trip-planner/SKILL.md): Full trip planning. Flights + hotels + points in one shot.