@nomadamas/k-skill 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/k-skill.js +60 -0
- package/package.json +37 -0
- package/skills/assembly-bill-vote-search/instruction.md +111 -0
- package/skills/assembly-bill-vote-search/skill.json +10 -0
- package/skills/biz-health-check/instruction.md +69 -0
- package/skills/biz-health-check/scripts/biz_health_check.py +163 -0
- package/skills/biz-health-check/skill.json +10 -0
- package/skills/bok-ecos-stats/instruction.md +95 -0
- package/skills/bok-ecos-stats/scripts/bok_ecos.py +302 -0
- package/skills/bok-ecos-stats/skill.json +10 -0
- package/skills/building-register-search/instruction.md +69 -0
- package/skills/building-register-search/scripts/building_register.py +309 -0
- package/skills/building-register-search/scripts/building_register_xml.py +32 -0
- package/skills/building-register-search/skill.json +9 -0
- package/skills/bunjang-search/instruction.md +154 -0
- package/skills/bunjang-search/skill.json +10 -0
- package/skills/catchtable-sniper/instruction.md +270 -0
- package/skills/catchtable-sniper/skill.json +9 -0
- package/skills/cheap-gas-nearby/instruction.md +107 -0
- package/skills/cheap-gas-nearby/skill.json +10 -0
- package/skills/corporate-registration-consulting/instruction.md +117 -0
- package/skills/corporate-registration-consulting/scripts/fill_official_hwp.py +106 -0
- package/skills/corporate-registration-consulting/skill.json +8 -0
- package/skills/coupang-product-search/instruction.md +219 -0
- package/skills/coupang-product-search/scripts/coupang_partners_mcp.py +146 -0
- package/skills/coupang-product-search/skill.json +10 -0
- package/skills/court-auction-notice-search/instruction.md +201 -0
- package/skills/court-auction-notice-search/skill.json +9 -0
- package/skills/court-payment-order-assistant/instruction.md +120 -0
- package/skills/court-payment-order-assistant/skill.json +10 -0
- package/skills/d2b-notice-search/instruction.md +117 -0
- package/skills/d2b-notice-search/skill.json +10 -0
- package/skills/daangn-cars-search/instruction.md +91 -0
- package/skills/daangn-cars-search/scripts/daangn_cars.py +72 -0
- package/skills/daangn-cars-search/skill.json +9 -0
- package/skills/daangn-jobs-search/instruction.md +90 -0
- package/skills/daangn-jobs-search/scripts/daangn_jobs.py +98 -0
- package/skills/daangn-jobs-search/skill.json +9 -0
- package/skills/daangn-realty-search/instruction.md +114 -0
- package/skills/daangn-realty-search/scripts/daangn_detail_ld.py +76 -0
- package/skills/daangn-realty-search/scripts/daangn_realty.py +216 -0
- package/skills/daangn-realty-search/scripts/daangn_relay_store.py +174 -0
- package/skills/daangn-realty-search/skill.json +8 -0
- package/skills/daangn-used-goods-search/instruction.md +90 -0
- package/skills/daangn-used-goods-search/scripts/daangn_used_goods.py +80 -0
- package/skills/daangn-used-goods-search/skill.json +9 -0
- package/skills/daishin-report-search/instruction.md +138 -0
- package/skills/daishin-report-search/skill.json +10 -0
- package/skills/daiso-product-search/instruction.md +172 -0
- package/skills/daiso-product-search/skill.json +8 -0
- package/skills/danawa-price-search/instruction.md +183 -0
- package/skills/danawa-price-search/scripts/danawa_search.py +354 -0
- package/skills/danawa-price-search/skill.json +8 -0
- package/skills/delivery-tracking/instruction.md +358 -0
- package/skills/delivery-tracking/skill.json +8 -0
- package/skills/donation-place-search/instruction.md +129 -0
- package/skills/donation-place-search/skill.json +9 -0
- package/skills/emergency-room-beds/instruction.md +82 -0
- package/skills/emergency-room-beds/skill.json +8 -0
- package/skills/ev-charger-nearby/instruction.md +80 -0
- package/skills/ev-charger-nearby/scripts/ev_charger.py +222 -0
- package/skills/ev-charger-nearby/skill.json +10 -0
- package/skills/ev-subsidy-status/instruction.md +165 -0
- package/skills/ev-subsidy-status/skill.json +10 -0
- package/skills/express-bus-booking/instruction.md +207 -0
- package/skills/express-bus-booking/references/kobus-http-flow.md +159 -0
- package/skills/express-bus-booking/scripts/kobus_express_booking.py +243 -0
- package/skills/express-bus-booking/skill.json +8 -0
- package/skills/fine-dust-location/instruction.md +89 -0
- package/skills/fine-dust-location/skill.json +9 -0
- package/skills/flight-ticket-search/instruction.md +237 -0
- package/skills/flight-ticket-search/scripts/flight_ticket_search.py +501 -0
- package/skills/flight-ticket-search/skill.json +9 -0
- package/skills/foresttrip-vacancy/instruction.md +167 -0
- package/skills/foresttrip-vacancy/scripts/run_foresttrip_vacancy.py +549 -0
- package/skills/foresttrip-vacancy/skill.json +10 -0
- package/skills/fsc-corporate-info/instruction.md +57 -0
- package/skills/fsc-corporate-info/scripts/fsc_corporate_info.py +113 -0
- package/skills/fsc-corporate-info/skill.json +10 -0
- package/skills/g2b-order-plan-search/instruction.md +131 -0
- package/skills/g2b-order-plan-search/scripts/g2b_order_plan.py +158 -0
- package/skills/g2b-order-plan-search/skill.json +10 -0
- package/skills/g2b-sanctioned-supplier/instruction.md +61 -0
- package/skills/g2b-sanctioned-supplier/scripts/g2b_sanctioned_supplier.py +114 -0
- package/skills/g2b-sanctioned-supplier/skill.json +10 -0
- package/skills/gangnamunni-clinic-search/instruction.md +113 -0
- package/skills/gangnamunni-clinic-search/skill.json +9 -0
- package/skills/geeknews-search/instruction.md +69 -0
- package/skills/geeknews-search/scripts/geeknews_search.py +296 -0
- package/skills/geeknews-search/skill.json +8 -0
- package/skills/gongsijiga-search/instruction.md +128 -0
- package/skills/gongsijiga-search/skill.json +9 -0
- package/skills/gov-overseas-trip-report/instruction.md +488 -0
- package/skills/gov-overseas-trip-report/scripts/gov_overseas_trip_report.py +1206 -0
- package/skills/gov-overseas-trip-report/skill.json +9 -0
- package/skills/han-river-water-level/instruction.md +83 -0
- package/skills/han-river-water-level/skill.json +10 -0
- package/skills/highway-traffic-status/instruction.md +88 -0
- package/skills/highway-traffic-status/scripts/highway_traffic.py +315 -0
- package/skills/highway-traffic-status/skill.json +11 -0
- package/skills/hipass-receipt/instruction.md +97 -0
- package/skills/hipass-receipt/skill.json +10 -0
- package/skills/hola-poke-yeoksam/instruction.md +247 -0
- package/skills/hola-poke-yeoksam/skill.json +8 -0
- package/skills/household-waste-info/instruction.md +117 -0
- package/skills/household-waste-info/skill.json +10 -0
- package/skills/housing-official-price/instruction.md +177 -0
- package/skills/housing-official-price/skill.json +10 -0
- package/skills/hwp/instruction.md +206 -0
- package/skills/hwp/skill.json +8 -0
- package/skills/intercity-bus-booking/instruction.md +189 -0
- package/skills/intercity-bus-booking/references/tmoney-intercity-http-flow.md +126 -0
- package/skills/intercity-bus-booking/scripts/intercity_bus_search.py +381 -0
- package/skills/intercity-bus-booking/skill.json +8 -0
- package/skills/iros-registry-automation/instruction.md +229 -0
- package/skills/iros-registry-automation/scripts/iros_pdf_summary.py +249 -0
- package/skills/iros-registry-automation/scripts/upstream.pin +1 -0
- package/skills/iros-registry-automation/skill.json +9 -0
- package/skills/job-posting-match/instruction.md +130 -0
- package/skills/job-posting-match/scripts/job_posting_match.py +396 -0
- package/skills/job-posting-match/scripts/test_job_posting_match.py +54 -0
- package/skills/job-posting-match/skill.json +8 -0
- package/skills/jobkorea-talent-search/instruction.md +118 -0
- package/skills/jobkorea-talent-search/scripts/jobkorea_talent_models.py +27 -0
- package/skills/jobkorea-talent-search/scripts/jobkorea_talent_parse.py +186 -0
- package/skills/jobkorea-talent-search/scripts/jobkorea_talent_search.py +94 -0
- package/skills/jobkorea-talent-search/scripts/jobkorea_talent_search_condition.py +136 -0
- package/skills/jobkorea-talent-search/scripts/test_jobkorea_talent_search.py +76 -0
- package/skills/jobkorea-talent-search/skill.json +9 -0
- package/skills/joseon-sillok-search/instruction.md +76 -0
- package/skills/joseon-sillok-search/scripts/sillok_search.py +552 -0
- package/skills/joseon-sillok-search/skill.json +8 -0
- package/skills/k-dart/instruction.md +406 -0
- package/skills/k-dart/skill.json +8 -0
- package/skills/k-schoollunch-menu/instruction.md +109 -0
- package/skills/k-schoollunch-menu/skill.json +9 -0
- package/skills/k-skill-cleaner/instruction.md +80 -0
- package/skills/k-skill-cleaner/scripts/k_skill_cleaner.py +410 -0
- package/skills/k-skill-cleaner/skill.json +8 -0
- package/skills/k-skill-setup/instruction.md +253 -0
- package/skills/k-skill-setup/skill.json +11 -0
- package/skills/kakao-bar-nearby/instruction.md +76 -0
- package/skills/kakao-bar-nearby/skill.json +8 -0
- package/skills/kakao-map/instruction.md +176 -0
- package/skills/kakao-map/skill.json +10 -0
- package/skills/kakaotalk-mac/instruction.md +189 -0
- package/skills/kakaotalk-mac/skill.json +8 -0
- package/skills/kbl-results/instruction.md +89 -0
- package/skills/kbl-results/skill.json +9 -0
- package/skills/kbo-results/instruction.md +82 -0
- package/skills/kbo-results/skill.json +8 -0
- package/skills/keris-academic-search/instruction.md +81 -0
- package/skills/keris-academic-search/scripts/keris_academic.py +210 -0
- package/skills/keris-academic-search/skill.json +10 -0
- package/skills/kleague-results/instruction.md +92 -0
- package/skills/kleague-results/skill.json +8 -0
- package/skills/kopis-performance-search/instruction.md +109 -0
- package/skills/kopis-performance-search/skill.json +10 -0
- package/skills/korea-weather/instruction.md +93 -0
- package/skills/korea-weather/skill.json +10 -0
- package/skills/korean-character-count/instruction.md +87 -0
- package/skills/korean-character-count/scripts/korean_character_count.js +268 -0
- package/skills/korean-character-count/skill.json +8 -0
- package/skills/korean-cinema-search/instruction.md +177 -0
- package/skills/korean-cinema-search/skill.json +8 -0
- package/skills/korean-heritage-search/instruction.md +106 -0
- package/skills/korean-heritage-search/scripts/korean_heritage_search.py +321 -0
- package/skills/korean-heritage-search/skill.json +8 -0
- package/skills/korean-holiday-calendar/instruction.md +97 -0
- package/skills/korean-holiday-calendar/skill.json +9 -0
- package/skills/korean-humanizer/instruction.md +389 -0
- package/skills/korean-humanizer/references/ai-tell-taxonomy.md +147 -0
- package/skills/korean-humanizer/skill.json +8 -0
- package/skills/korean-jangbu-for/instruction.md +133 -0
- package/skills/korean-jangbu-for/scripts/install.sh +237 -0
- package/skills/korean-jangbu-for/scripts/upstream.pin +1 -0
- package/skills/korean-jangbu-for/skill.json +8 -0
- package/skills/korean-law-search/instruction.md +126 -0
- package/skills/korean-law-search/skill.json +10 -0
- package/skills/korean-marathon-schedule/instruction.md +111 -0
- package/skills/korean-marathon-schedule/skill.json +9 -0
- package/skills/korean-middle-korean/instruction.md +79 -0
- package/skills/korean-middle-korean/scripts/korean_middle_korean.js +214 -0
- package/skills/korean-middle-korean/skill.json +8 -0
- package/skills/korean-patent-search/instruction.md +79 -0
- package/skills/korean-patent-search/scripts/patent_search.py +409 -0
- package/skills/korean-patent-search/skill.json +9 -0
- package/skills/korean-privacy-terms/instruction.md +128 -0
- package/skills/korean-privacy-terms/scripts/install.sh +108 -0
- package/skills/korean-privacy-terms/scripts/upstream.pin +1 -0
- package/skills/korean-privacy-terms/skill.json +8 -0
- package/skills/korean-scholarship-search/instruction.md +317 -0
- package/skills/korean-scholarship-search/references/report-format.md +40 -0
- package/skills/korean-scholarship-search/references/school-discovery.md +61 -0
- package/skills/korean-scholarship-search/references/search-clues.md +58 -0
- package/skills/korean-scholarship-search/references/source-patterns.md +67 -0
- package/skills/korean-scholarship-search/scripts/scholarship_filter.py +811 -0
- package/skills/korean-scholarship-search/scripts/test_scholarship_filter.py +224 -0
- package/skills/korean-scholarship-search/scripts/university_search_plan.py +148 -0
- package/skills/korean-scholarship-search/skill.json +8 -0
- package/skills/korean-slang-writing/instruction.md +181 -0
- package/skills/korean-slang-writing/scripts/_slang_http.py +91 -0
- package/skills/korean-slang-writing/scripts/slang_lookup.py +291 -0
- package/skills/korean-slang-writing/scripts/slang_search.py +284 -0
- package/skills/korean-slang-writing/skill.json +8 -0
- package/skills/korean-spell-check/instruction.md +105 -0
- package/skills/korean-spell-check/scripts/korean_spell_check.py +523 -0
- package/skills/korean-spell-check/skill.json +9 -0
- package/skills/korean-stock-search/instruction.md +194 -0
- package/skills/korean-stock-search/skill.json +10 -0
- package/skills/korean-transit-route/instruction.md +113 -0
- package/skills/korean-transit-route/skill.json +10 -0
- package/skills/kosis-stats/instruction.md +232 -0
- package/skills/kosis-stats/references/kosis-openapi-guide.md +171 -0
- package/skills/kosis-stats/scripts/run_kosis_stats.py +896 -0
- package/skills/kosis-stats/skill.json +9 -0
- package/skills/kr-whois-lookup/instruction.md +107 -0
- package/skills/kr-whois-lookup/skill.json +10 -0
- package/skills/kstartup-search/instruction.md +186 -0
- package/skills/kstartup-search/scripts/run_kstartup.py +424 -0
- package/skills/kstartup-search/skill.json +11 -0
- package/skills/ktx-booking/instruction.md +244 -0
- package/skills/ktx-booking/skill.json +10 -0
- package/skills/lck-analytics/instruction.md +192 -0
- package/skills/lck-analytics/scripts/_lib.js +103 -0
- package/skills/lck-analytics/scripts/analyze-live-game.js +52 -0
- package/skills/lck-analytics/scripts/build-match-report.js +44 -0
- package/skills/lck-analytics/scripts/sync-oracle.js +50 -0
- package/skills/lck-analytics/skill.json +9 -0
- package/skills/lh-notice-search/instruction.md +206 -0
- package/skills/lh-notice-search/skill.json +10 -0
- package/skills/library-book-search/instruction.md +139 -0
- package/skills/library-book-search/skill.json +9 -0
- package/skills/local-election-candidate-search/instruction.md +77 -0
- package/skills/local-election-candidate-search/skill.json +8 -0
- package/skills/localdata-business-status/instruction.md +64 -0
- package/skills/localdata-business-status/scripts/localdata_business_status.py +206 -0
- package/skills/localdata-business-status/skill.json +8 -0
- package/skills/lotto-results/instruction.md +80 -0
- package/skills/lotto-results/skill.json +8 -0
- package/skills/lovebug-report/instruction.md +185 -0
- package/skills/lovebug-report/skill.json +9 -0
- package/skills/market-kurly-search/instruction.md +125 -0
- package/skills/market-kurly-search/skill.json +8 -0
- package/skills/mfds-drug-safety/instruction.md +86 -0
- package/skills/mfds-drug-safety/scripts/mfds_drug_safety.py +184 -0
- package/skills/mfds-drug-safety/skill.json +10 -0
- package/skills/mfds-food-safety/instruction.md +126 -0
- package/skills/mfds-food-safety/scripts/mfds_food_safety.py +281 -0
- package/skills/mfds-food-safety/skill.json +10 -0
- package/skills/myrealtrip-search/instruction.md +239 -0
- package/skills/myrealtrip-search/scripts/myrealtrip_mcp.py +194 -0
- package/skills/myrealtrip-search/scripts/test_myrealtrip_mcp.py +99 -0
- package/skills/myrealtrip-search/skill.json +9 -0
- package/skills/naming-house/instruction.md +146 -0
- package/skills/naming-house/skill.json +8 -0
- package/skills/national-pension-workplace/instruction.md +64 -0
- package/skills/national-pension-workplace/scripts/national_pension_workplace.py +113 -0
- package/skills/national-pension-workplace/skill.json +10 -0
- package/skills/naver-ad-performance/instruction.md +108 -0
- package/skills/naver-ad-performance/scripts/naver_ad_performance.py +240 -0
- package/skills/naver-ad-performance/skill.json +9 -0
- package/skills/naver-blog-research/instruction.md +128 -0
- package/skills/naver-blog-research/scripts/_naver_http.py +58 -0
- package/skills/naver-blog-research/scripts/naver_download_images.py +233 -0
- package/skills/naver-blog-research/scripts/naver_read.py +256 -0
- package/skills/naver-blog-research/scripts/naver_search.py +192 -0
- package/skills/naver-blog-research/skill.json +8 -0
- package/skills/naver-news-search/instruction.md +103 -0
- package/skills/naver-news-search/skill.json +10 -0
- package/skills/naver-shopping-search/instruction.md +94 -0
- package/skills/naver-shopping-search/skill.json +11 -0
- package/skills/nhis-care-checkup-search/instruction.md +116 -0
- package/skills/nhis-care-checkup-search/skill.json +10 -0
- package/skills/nts-business-registration/instruction.md +115 -0
- package/skills/nts-business-registration/scripts/nts_business_registration.py +215 -0
- package/skills/nts-business-registration/skill.json +10 -0
- package/skills/nts-tax-delinquency/instruction.md +54 -0
- package/skills/nts-tax-delinquency/scripts/nts_tax_delinquency.py +150 -0
- package/skills/nts-tax-delinquency/skill.json +8 -0
- package/skills/ohou-today-deal/instruction.md +182 -0
- package/skills/ohou-today-deal/scripts/ohou_today_deal.py +369 -0
- package/skills/ohou-today-deal/skill.json +9 -0
- package/skills/olive-young-search/instruction.md +154 -0
- package/skills/olive-young-search/skill.json +8 -0
- package/skills/parking-lot-search/instruction.md +96 -0
- package/skills/parking-lot-search/skill.json +9 -0
- package/skills/popbill/instruction.md +146 -0
- package/skills/popbill/scripts/popbill_cli.py +284 -0
- package/skills/popbill/scripts/popbill_registry.py +54 -0
- package/skills/popbill/scripts/popbill_safety.py +37 -0
- package/skills/popbill/scripts/popbill_templates.py +71 -0
- package/skills/popbill/skill.json +9 -0
- package/skills/public-restroom-nearby/instruction.md +89 -0
- package/skills/public-restroom-nearby/skill.json +9 -0
- package/skills/real-estate-search/instruction.md +172 -0
- package/skills/real-estate-search/skill.json +10 -0
- package/skills/rhwp-advanced/instruction.md +145 -0
- package/skills/rhwp-advanced/skill.json +8 -0
- package/skills/rhwp-edit/instruction.md +153 -0
- package/skills/rhwp-edit/skill.json +8 -0
- package/skills/s2b-notice-search/instruction.md +66 -0
- package/skills/s2b-notice-search/skill.json +9 -0
- package/skills/saju-fortune/instruction.md +177 -0
- package/skills/saju-fortune/skill.json +8 -0
- package/skills/saramin-talent-search/instruction.md +119 -0
- package/skills/saramin-talent-search/skill.json +9 -0
- package/skills/seoul-bike/instruction.md +83 -0
- package/skills/seoul-bike/scripts/seoul_bike.py +247 -0
- package/skills/seoul-bike/skill.json +10 -0
- package/skills/seoul-density/instruction.md +109 -0
- package/skills/seoul-density/scripts/seoul_density.py +271 -0
- package/skills/seoul-density/skill.json +10 -0
- package/skills/seoul-subway-arrival/instruction.md +85 -0
- package/skills/seoul-subway-arrival/skill.json +9 -0
- package/skills/sh-notice-search/instruction.md +150 -0
- package/skills/sh-notice-search/skill.json +9 -0
- package/skills/srt-booking/instruction.md +181 -0
- package/skills/srt-booking/scripts/srt_booking.py +272 -0
- package/skills/srt-booking/scripts/srt_seats.py +156 -0
- package/skills/srt-booking/skill.json +20 -0
- package/skills/subway-lost-property/instruction.md +93 -0
- package/skills/subway-lost-property/scripts/subway_lost_property.py +244 -0
- package/skills/subway-lost-property/skill.json +8 -0
- package/skills/ticket-availability/instruction.md +175 -0
- package/skills/ticket-availability/scripts/ticket_availability.py +430 -0
- package/skills/ticket-availability/skill.json +9 -0
- package/skills/toss-securities/instruction.md +116 -0
- package/skills/toss-securities/skill.json +9 -0
- package/skills/used-car-price-search/instruction.md +109 -0
- package/skills/used-car-price-search/skill.json +8 -0
- package/skills/yebigun-training/instruction.md +177 -0
- package/skills/yebigun-training/skill.json +9 -0
- package/skills/zipcode-search/instruction.md +159 -0
- package/skills/zipcode-search/scripts/zipcode_search.py +150 -0
- package/skills/zipcode-search/skill.json +8 -0
- package/src/assemble.js +134 -0
- package/src/detect.js +18 -0
- package/templates/action-account.md +4 -0
- package/templates/action-booking.md +5 -0
- package/templates/action-commerce.md +5 -0
- package/templates/action-communication.md +5 -0
- package/templates/action-submission.md +5 -0
- package/templates/browser.md +4 -0
- package/templates/core.md +5 -0
- package/templates/hard-boundary.md +3 -0
- package/templates/local.md +2 -0
- package/templates/lookup.md +2 -0
- package/templates/proxy.md +2 -0
- package/templates/vault.md +4 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Free flight ticket search helper for the k-skill flight-ticket-search skill.
|
|
3
|
+
|
|
4
|
+
Uses fast-flights (Google Flights public surface scraper) in an isolated user cache venv.
|
|
5
|
+
No API key, login, CAPTCHA bypass, purchase, or booking automation.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import statistics
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
from dataclasses import asdict
|
|
19
|
+
from datetime import date, datetime, timedelta
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Iterable
|
|
22
|
+
from urllib.parse import urlencode
|
|
23
|
+
|
|
24
|
+
PINNED_FAST_FLIGHTS = "fast-flights==2.2"
|
|
25
|
+
CACHE_ROOT = Path.home() / ".cache" / "k-skill" / "flight-ticket-search"
|
|
26
|
+
VENV_DIR = CACHE_ROOT / "venv"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def ensure_runtime() -> None:
|
|
30
|
+
"""Install fast-flights into a private cache venv, then re-exec there."""
|
|
31
|
+
if os.environ.get("FLIGHT_TICKET_SEARCH_BOOTSTRAPPED") == "1":
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
py = VENV_DIR / "bin" / "python"
|
|
35
|
+
|
|
36
|
+
def candidate_python_executables() -> list[str]:
|
|
37
|
+
candidates = [sys.executable]
|
|
38
|
+
candidates.extend(
|
|
39
|
+
found for name in ("python3.13", "python3.12", "python3.11", "python3")
|
|
40
|
+
if (found := shutil.which(name))
|
|
41
|
+
)
|
|
42
|
+
seen: set[str] = set()
|
|
43
|
+
unique: list[str] = []
|
|
44
|
+
for candidate in candidates:
|
|
45
|
+
resolved = str(Path(candidate).resolve())
|
|
46
|
+
if resolved not in seen:
|
|
47
|
+
seen.add(resolved)
|
|
48
|
+
unique.append(candidate)
|
|
49
|
+
return unique
|
|
50
|
+
|
|
51
|
+
def create_venv() -> None:
|
|
52
|
+
CACHE_ROOT.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
errors: list[str] = []
|
|
54
|
+
for python in candidate_python_executables():
|
|
55
|
+
shutil.rmtree(VENV_DIR, ignore_errors=True)
|
|
56
|
+
try:
|
|
57
|
+
subprocess.check_call([python, "-m", "venv", str(VENV_DIR)])
|
|
58
|
+
return
|
|
59
|
+
except (OSError, subprocess.CalledProcessError) as exc:
|
|
60
|
+
errors.append(f"{python}: {exc}")
|
|
61
|
+
raise RuntimeError(
|
|
62
|
+
"Unable to create flight-ticket-search venv with available Python interpreters: "
|
|
63
|
+
+ "; ".join(errors)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def venv_has_fast_flights() -> bool:
|
|
67
|
+
if not py.exists():
|
|
68
|
+
return False
|
|
69
|
+
return subprocess.run(
|
|
70
|
+
[str(py), "-c", "import fast_flights"],
|
|
71
|
+
stdout=subprocess.DEVNULL,
|
|
72
|
+
stderr=subprocess.DEVNULL,
|
|
73
|
+
check=False,
|
|
74
|
+
).returncode == 0
|
|
75
|
+
|
|
76
|
+
if not py.exists():
|
|
77
|
+
create_venv()
|
|
78
|
+
|
|
79
|
+
if not venv_has_fast_flights():
|
|
80
|
+
try:
|
|
81
|
+
subprocess.check_call([str(py), "-m", "ensurepip", "--upgrade"])
|
|
82
|
+
subprocess.check_call([
|
|
83
|
+
str(py),
|
|
84
|
+
"-m",
|
|
85
|
+
"pip",
|
|
86
|
+
"install",
|
|
87
|
+
"--disable-pip-version-check",
|
|
88
|
+
"-q",
|
|
89
|
+
PINNED_FAST_FLIGHTS,
|
|
90
|
+
])
|
|
91
|
+
except (OSError, subprocess.CalledProcessError):
|
|
92
|
+
# Recover from interrupted or pip-less cache venvs before surfacing a hard failure.
|
|
93
|
+
shutil.rmtree(VENV_DIR, ignore_errors=True)
|
|
94
|
+
create_venv()
|
|
95
|
+
subprocess.check_call([str(py), "-m", "ensurepip", "--upgrade"])
|
|
96
|
+
subprocess.check_call([
|
|
97
|
+
str(py),
|
|
98
|
+
"-m",
|
|
99
|
+
"pip",
|
|
100
|
+
"install",
|
|
101
|
+
"--disable-pip-version-check",
|
|
102
|
+
"-q",
|
|
103
|
+
PINNED_FAST_FLIGHTS,
|
|
104
|
+
])
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
env = os.environ.copy()
|
|
108
|
+
env["FLIGHT_TICKET_SEARCH_BOOTSTRAPPED"] = "1"
|
|
109
|
+
os.execve(str(py), [str(py), __file__, *sys.argv[1:]], env)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def parse_date(s: str) -> date:
|
|
113
|
+
return datetime.strptime(s, "%Y-%m-%d").date()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def positive_int(value: str) -> int:
|
|
117
|
+
parsed = int(value)
|
|
118
|
+
if parsed < 1:
|
|
119
|
+
raise argparse.ArgumentTypeError("must be a positive integer")
|
|
120
|
+
return parsed
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def nonnegative_int(value: str) -> int:
|
|
124
|
+
parsed = int(value)
|
|
125
|
+
if parsed < 0:
|
|
126
|
+
raise argparse.ArgumentTypeError("must be zero or a positive integer")
|
|
127
|
+
return parsed
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def nonnegative_float(value: str) -> float:
|
|
131
|
+
parsed = float(value)
|
|
132
|
+
if parsed < 0:
|
|
133
|
+
raise argparse.ArgumentTypeError("must be zero or a positive number")
|
|
134
|
+
return parsed
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def iter_dates(start: date, end: date, step_days: int) -> Iterable[date]:
|
|
139
|
+
d = start
|
|
140
|
+
while d <= end:
|
|
141
|
+
yield d
|
|
142
|
+
d += timedelta(days=step_days)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def parse_price(price_text: str | None) -> int | None:
|
|
146
|
+
if not price_text or "unavailable" in price_text.lower():
|
|
147
|
+
return None
|
|
148
|
+
digits = re.sub(r"[^0-9]", "", price_text)
|
|
149
|
+
return int(digits) if digits else None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def money_krw(value: int | float | None) -> str:
|
|
153
|
+
if value is None:
|
|
154
|
+
return "ํ์ธ ๋ถ๊ฐ"
|
|
155
|
+
return f"โฉ{int(round(value)):,}"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def validate_airport(code: str, field: str) -> str:
|
|
159
|
+
code = code.strip().upper()
|
|
160
|
+
if not re.fullmatch(r"[A-Z]{3}", code):
|
|
161
|
+
raise SystemExit(f"{field} must be a 3-letter IATA airport code, got: {code!r}")
|
|
162
|
+
return code
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def build_query_url(flight_data: list[Any], trip: str, adults: int, seat: str) -> str:
|
|
166
|
+
from fast_flights.flights_impl import Passengers, TFSData
|
|
167
|
+
|
|
168
|
+
tfs = TFSData.from_interface(
|
|
169
|
+
flight_data=flight_data,
|
|
170
|
+
trip=trip,
|
|
171
|
+
passengers=Passengers(adults=adults),
|
|
172
|
+
seat=seat,
|
|
173
|
+
).as_b64().decode("utf-8")
|
|
174
|
+
params = {"tfs": tfs, "hl": "en", "tfu": "EgQIABABIgA", "curr": "KRW"}
|
|
175
|
+
return "https://www.google.com/travel/flights?" + urlencode(params)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def make_flight_data(from_airport: str, to_airport: str, outbound: str, return_date: str | None = None) -> tuple[list[Any], str]:
|
|
179
|
+
origin = validate_airport(from_airport, "from")
|
|
180
|
+
dest = validate_airport(to_airport, "to")
|
|
181
|
+
if origin == dest:
|
|
182
|
+
raise SystemExit("from and to airports must be different")
|
|
183
|
+
outbound_date = parse_date(outbound)
|
|
184
|
+
|
|
185
|
+
from fast_flights import FlightData
|
|
186
|
+
|
|
187
|
+
data = [FlightData(date=outbound_date.isoformat(), from_airport=origin, to_airport=dest)]
|
|
188
|
+
if return_date:
|
|
189
|
+
inbound_date = parse_date(return_date)
|
|
190
|
+
if inbound_date < outbound_date:
|
|
191
|
+
raise SystemExit("return-date must be on or after date")
|
|
192
|
+
data.append(FlightData(date=inbound_date.isoformat(), from_airport=dest, to_airport=origin))
|
|
193
|
+
return data, "round-trip"
|
|
194
|
+
return data, "one-way"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def fetch_flights(flight_data: list[Any], trip: str, adults: int, seat: str) -> Any:
|
|
198
|
+
from fast_flights import Passengers, get_flights
|
|
199
|
+
|
|
200
|
+
return get_flights(
|
|
201
|
+
flight_data=flight_data,
|
|
202
|
+
trip=trip,
|
|
203
|
+
passengers=Passengers(adults=adults),
|
|
204
|
+
seat=seat,
|
|
205
|
+
fetch_mode="fallback",
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def normalize_flight(f: Any) -> dict[str, Any]:
|
|
210
|
+
raw = asdict(f)
|
|
211
|
+
price_value = parse_price(raw.get("price"))
|
|
212
|
+
raw["price_value"] = price_value
|
|
213
|
+
raw["price_text"] = money_krw(price_value) if price_value is not None else raw.get("price") or "ํ์ธ ๋ถ๊ฐ"
|
|
214
|
+
raw["quality"] = "complete" if raw.get("name") and raw.get("departure") and raw.get("arrival") else "partial"
|
|
215
|
+
return raw
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def summarize_result(res: Any, query_url: str, limit: int) -> dict[str, Any]:
|
|
219
|
+
flights = [normalize_flight(f) for f in res.flights]
|
|
220
|
+
priced = [f for f in flights if f["price_value"] is not None]
|
|
221
|
+
complete = [f for f in priced if f["quality"] == "complete"]
|
|
222
|
+
best_pool = complete or priced
|
|
223
|
+
best_pool = sorted(best_pool, key=lambda x: x["price_value"] if x["price_value"] is not None else 10**18)
|
|
224
|
+
values = [f["price_value"] for f in priced if f["price_value"] is not None]
|
|
225
|
+
return {
|
|
226
|
+
"meta": {
|
|
227
|
+
"provider": "google-flights-fast-flights",
|
|
228
|
+
"source": "Google Flights public search surface via fast-flights",
|
|
229
|
+
"price_band": getattr(res, "current_price", ""),
|
|
230
|
+
"currency": "KRW",
|
|
231
|
+
"queried_at": datetime.now().astimezone().isoformat(timespec="seconds"),
|
|
232
|
+
"booking_search_url": query_url,
|
|
233
|
+
"note": "์์ฝ ๋งํฌ๋ ํน์ ํ๋งค์ ๊ฒฐ์ deep link๊ฐ ์๋๋ผ Google Flights ๊ฒ์ ๊ฒฐ๊ณผ ๋งํฌ์
๋๋ค.",
|
|
234
|
+
},
|
|
235
|
+
"stats": {
|
|
236
|
+
"result_count": len(flights),
|
|
237
|
+
"priced_count": len(priced),
|
|
238
|
+
"complete_count": len(complete),
|
|
239
|
+
"min_price": min(values) if values else None,
|
|
240
|
+
"avg_price": statistics.mean(values) if values else None,
|
|
241
|
+
"max_price": max(values) if values else None,
|
|
242
|
+
},
|
|
243
|
+
"flights": best_pool[:limit],
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def validate_date_text(value: str, field: str) -> date:
|
|
249
|
+
try:
|
|
250
|
+
return parse_date(value)
|
|
251
|
+
except ValueError as exc:
|
|
252
|
+
raise SystemExit(f"{field} must be YYYY-MM-DD, got: {value!r}") from exc
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def validate_month_text(value: str) -> None:
|
|
256
|
+
try:
|
|
257
|
+
datetime.strptime(value + "-01", "%Y-%m-%d")
|
|
258
|
+
except ValueError as exc:
|
|
259
|
+
raise SystemExit(f"month must be YYYY-MM, got: {value!r}") from exc
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def validate_month_day_text(value: str) -> None:
|
|
263
|
+
try:
|
|
264
|
+
datetime.strptime("2000-" + value, "%Y-%m-%d")
|
|
265
|
+
except ValueError as exc:
|
|
266
|
+
raise SystemExit(f"month-day must be MM-DD, got: {value!r}") from exc
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def preflight_validate_args(args: argparse.Namespace) -> None:
|
|
270
|
+
validate_airport(args.from_airport, "from")
|
|
271
|
+
validate_airport(args.to_airport, "to")
|
|
272
|
+
if args.from_airport.strip().upper() == args.to_airport.strip().upper():
|
|
273
|
+
raise SystemExit("from and to airports must be different")
|
|
274
|
+
|
|
275
|
+
if args.command == "search":
|
|
276
|
+
outbound = validate_date_text(args.date, "date")
|
|
277
|
+
if args.return_date:
|
|
278
|
+
inbound = validate_date_text(args.return_date, "return-date")
|
|
279
|
+
if inbound < outbound:
|
|
280
|
+
raise SystemExit("return-date must be on or after date")
|
|
281
|
+
elif args.command == "compare-month":
|
|
282
|
+
validate_month_text(args.month)
|
|
283
|
+
elif args.command == "compare-range":
|
|
284
|
+
start = validate_date_text(args.start_date, "start-date")
|
|
285
|
+
end = validate_date_text(args.end_date, "end-date")
|
|
286
|
+
if end < start:
|
|
287
|
+
raise SystemExit("end-date must be on or after start-date")
|
|
288
|
+
elif args.command == "compare-years":
|
|
289
|
+
validate_month_day_text(args.month_day)
|
|
290
|
+
try:
|
|
291
|
+
years = [int(x) for x in re.split(r"[, ]+", args.years.strip()) if x]
|
|
292
|
+
except ValueError as exc:
|
|
293
|
+
raise SystemExit("years must be comma-separated numbers, e.g. 2026,2027") from exc
|
|
294
|
+
if not years:
|
|
295
|
+
raise SystemExit("years is required, e.g. 2026,2027")
|
|
296
|
+
|
|
297
|
+
def command_search(args: argparse.Namespace) -> dict[str, Any]:
|
|
298
|
+
data, trip = make_flight_data(args.from_airport, args.to_airport, args.date, args.return_date)
|
|
299
|
+
res = fetch_flights(data, trip, args.adults, args.seat)
|
|
300
|
+
url = build_query_url(data, trip, args.adults, args.seat)
|
|
301
|
+
out = summarize_result(res, url, args.limit)
|
|
302
|
+
out["query"] = {
|
|
303
|
+
"from": args.from_airport.upper(),
|
|
304
|
+
"to": args.to_airport.upper(),
|
|
305
|
+
"date": args.date,
|
|
306
|
+
"return_date": args.return_date,
|
|
307
|
+
"trip": trip,
|
|
308
|
+
"adults": args.adults,
|
|
309
|
+
"seat": args.seat,
|
|
310
|
+
}
|
|
311
|
+
return out
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def scan_dates(args: argparse.Namespace, dates: list[date]) -> dict[str, Any]:
|
|
315
|
+
rows: list[dict[str, Any]] = []
|
|
316
|
+
for idx, d in enumerate(dates):
|
|
317
|
+
try:
|
|
318
|
+
data, trip = make_flight_data(args.from_airport, args.to_airport, d.isoformat(), None)
|
|
319
|
+
res = fetch_flights(data, trip, args.adults, args.seat)
|
|
320
|
+
url = build_query_url(data, trip, args.adults, args.seat)
|
|
321
|
+
summary = summarize_result(res, url, args.limit)
|
|
322
|
+
rows.append({
|
|
323
|
+
"date": d.isoformat(),
|
|
324
|
+
"ok": True,
|
|
325
|
+
"min_price": summary["stats"]["min_price"],
|
|
326
|
+
"avg_price": summary["stats"]["avg_price"],
|
|
327
|
+
"priced_count": summary["stats"]["priced_count"],
|
|
328
|
+
"price_band": summary["meta"]["price_band"],
|
|
329
|
+
"top": summary["flights"][: min(3, args.limit)],
|
|
330
|
+
"booking_search_url": url,
|
|
331
|
+
})
|
|
332
|
+
except Exception as e: # keep scans robust
|
|
333
|
+
rows.append({"date": d.isoformat(), "ok": False, "error": f"{type(e).__name__}: {str(e)[:300]}"})
|
|
334
|
+
if idx != len(dates) - 1 and args.sleep > 0:
|
|
335
|
+
time.sleep(args.sleep)
|
|
336
|
+
prices = [r["min_price"] for r in rows if r.get("ok") and r.get("min_price") is not None]
|
|
337
|
+
ok_rows = [r for r in rows if r.get("ok")]
|
|
338
|
+
cheapest = sorted((r for r in ok_rows if r.get("min_price") is not None), key=lambda r: r["min_price"])[: args.limit]
|
|
339
|
+
return {
|
|
340
|
+
"meta": {
|
|
341
|
+
"provider": "google-flights-fast-flights",
|
|
342
|
+
"currency": "KRW",
|
|
343
|
+
"queried_at": datetime.now().astimezone().isoformat(timespec="seconds"),
|
|
344
|
+
"sampled_dates": len(rows),
|
|
345
|
+
"successful_dates": len(ok_rows),
|
|
346
|
+
"note": "์/์ฐ๋ ๋น๊ต๋ ์ง์ ๋ ์ง๋ค์ ์ค์ ์กฐํํด ์ฐ์ถํ ์ํ ๊ธฐ๋ฐ ๋น๊ต์
๋๋ค. Google Flights ๊ฐ๊ฒฉ์ ์์ ๋ณ๋๋ฉ๋๋ค.",
|
|
347
|
+
},
|
|
348
|
+
"query": {
|
|
349
|
+
"from": args.from_airport.upper(),
|
|
350
|
+
"to": args.to_airport.upper(),
|
|
351
|
+
"adults": args.adults,
|
|
352
|
+
"seat": args.seat,
|
|
353
|
+
},
|
|
354
|
+
"stats": {
|
|
355
|
+
"min_price": min(prices) if prices else None,
|
|
356
|
+
"avg_of_daily_min": statistics.mean(prices) if prices else None,
|
|
357
|
+
"max_of_daily_min": max(prices) if prices else None,
|
|
358
|
+
},
|
|
359
|
+
"cheapest_dates": cheapest,
|
|
360
|
+
"rows": rows,
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def command_compare_month(args: argparse.Namespace) -> dict[str, Any]:
|
|
365
|
+
month_start = datetime.strptime(args.month + "-01", "%Y-%m-%d").date()
|
|
366
|
+
if month_start.month == 12:
|
|
367
|
+
month_end = date(month_start.year + 1, 1, 1) - timedelta(days=1)
|
|
368
|
+
else:
|
|
369
|
+
month_end = date(month_start.year, month_start.month + 1, 1) - timedelta(days=1)
|
|
370
|
+
step = 1 if args.sample == "daily" else 7
|
|
371
|
+
dates = list(iter_dates(month_start, month_end, step))
|
|
372
|
+
if args.max_dates:
|
|
373
|
+
dates = dates[: args.max_dates]
|
|
374
|
+
out = scan_dates(args, dates)
|
|
375
|
+
out["query"]["month"] = args.month
|
|
376
|
+
out["query"]["sample"] = args.sample
|
|
377
|
+
return out
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def command_compare_range(args: argparse.Namespace) -> dict[str, Any]:
|
|
381
|
+
start = parse_date(args.start_date)
|
|
382
|
+
end = parse_date(args.end_date)
|
|
383
|
+
if end < start:
|
|
384
|
+
raise SystemExit("end-date must be on or after start-date")
|
|
385
|
+
step = args.step_days
|
|
386
|
+
dates = list(iter_dates(start, end, step))
|
|
387
|
+
if args.max_dates:
|
|
388
|
+
dates = dates[: args.max_dates]
|
|
389
|
+
out = scan_dates(args, dates)
|
|
390
|
+
out["query"]["start_date"] = args.start_date
|
|
391
|
+
out["query"]["end_date"] = args.end_date
|
|
392
|
+
out["query"]["step_days"] = step
|
|
393
|
+
return out
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def command_compare_years(args: argparse.Namespace) -> dict[str, Any]:
|
|
397
|
+
years = [int(x) for x in re.split(r"[, ]+", args.years.strip()) if x]
|
|
398
|
+
if not years:
|
|
399
|
+
raise SystemExit("years is required, e.g. 2026,2027")
|
|
400
|
+
mmdd = args.month_day
|
|
401
|
+
dates = [datetime.strptime(f"{year}-{mmdd}", "%Y-%m-%d").date() for year in years]
|
|
402
|
+
out = scan_dates(args, dates)
|
|
403
|
+
out["query"]["years"] = years
|
|
404
|
+
out["query"]["month_day"] = mmdd
|
|
405
|
+
return out
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def print_markdown(payload: dict[str, Any]) -> None:
|
|
409
|
+
meta = payload.get("meta", {})
|
|
410
|
+
query = payload.get("query", {})
|
|
411
|
+
stats = payload.get("stats", {})
|
|
412
|
+
print(f"provider: {meta.get('provider')}")
|
|
413
|
+
print(f"queried_at: {meta.get('queried_at')}")
|
|
414
|
+
print(f"query: {json.dumps(query, ensure_ascii=False)}")
|
|
415
|
+
print()
|
|
416
|
+
if "flights" in payload:
|
|
417
|
+
print(f"price_band: {meta.get('price_band')}")
|
|
418
|
+
print(f"min/avg/max: {money_krw(stats.get('min_price'))} / {money_krw(stats.get('avg_price'))} / {money_krw(stats.get('max_price'))}")
|
|
419
|
+
print(f"booking_search_url: {meta.get('booking_search_url')}")
|
|
420
|
+
print("\nflights:")
|
|
421
|
+
for i, f in enumerate(payload.get("flights", []), 1):
|
|
422
|
+
print(f"{i}. {f.get('name') or 'ํญ๊ณตํธ ์์ธ ํ์ธ ๋ถ๊ฐ'} | {f.get('departure') or '์๊ฐ ํ์ธ ๋ถ๊ฐ'} -> {f.get('arrival') or '์๊ฐ ํ์ธ ๋ถ๊ฐ'} | {f.get('duration') or '์์์๊ฐ ํ์ธ ๋ถ๊ฐ'} | stops={f.get('stops')} | {f.get('price_text')}")
|
|
423
|
+
else:
|
|
424
|
+
print(f"sampled/success: {meta.get('sampled_dates')} / {meta.get('successful_dates')}")
|
|
425
|
+
print(f"min / avg(daily min) / max(daily min): {money_krw(stats.get('min_price'))} / {money_krw(stats.get('avg_of_daily_min'))} / {money_krw(stats.get('max_of_daily_min'))}")
|
|
426
|
+
print("\ncheapest_dates:")
|
|
427
|
+
for i, r in enumerate(payload.get("cheapest_dates", []), 1):
|
|
428
|
+
print(f"{i}. {r.get('date')} | min={money_krw(r.get('min_price'))} | avg={money_krw(r.get('avg_price'))} | band={r.get('price_band')} | {r.get('booking_search_url')}")
|
|
429
|
+
failures = [r for r in payload.get("rows", []) if not r.get("ok")]
|
|
430
|
+
if failures:
|
|
431
|
+
print("\nfailures:")
|
|
432
|
+
for r in failures[:5]:
|
|
433
|
+
print(f"- {r.get('date')}: {r.get('error')}")
|
|
434
|
+
if meta.get("note"):
|
|
435
|
+
print(f"\nnote: {meta.get('note')}")
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
439
|
+
p = argparse.ArgumentParser(description="Free Google Flights-based flight search and comparison helper")
|
|
440
|
+
sub = p.add_subparsers(dest="command", required=True)
|
|
441
|
+
|
|
442
|
+
def add_common(sp: argparse.ArgumentParser) -> None:
|
|
443
|
+
sp.add_argument("--from", dest="from_airport", required=True, help="IATA origin airport, e.g. ICN")
|
|
444
|
+
sp.add_argument("--to", dest="to_airport", required=True, help="IATA destination airport, e.g. NRT")
|
|
445
|
+
sp.add_argument("--adults", type=positive_int, default=1)
|
|
446
|
+
sp.add_argument("--seat", choices=["economy", "premium-economy", "business", "first"], default="economy")
|
|
447
|
+
sp.add_argument("--limit", type=positive_int, default=5)
|
|
448
|
+
sp.add_argument("--sleep", type=nonnegative_float, default=1.5, help="seconds between comparison queries")
|
|
449
|
+
|
|
450
|
+
sp.add_argument("--format", choices=["json", "markdown"], default="markdown")
|
|
451
|
+
|
|
452
|
+
s = sub.add_parser("search", help="single one-way or round-trip search")
|
|
453
|
+
add_common(s)
|
|
454
|
+
s.add_argument("--date", required=True, help="YYYY-MM-DD outbound date")
|
|
455
|
+
s.add_argument("--return-date", help="YYYY-MM-DD return date for round trip")
|
|
456
|
+
s.set_defaults(func=command_search)
|
|
457
|
+
|
|
458
|
+
m = sub.add_parser("compare-month", help="sample a whole month and rank cheapest dates")
|
|
459
|
+
add_common(m)
|
|
460
|
+
m.add_argument("--month", required=True, help="YYYY-MM")
|
|
461
|
+
m.add_argument("--sample", choices=["weekly", "daily"], default="weekly")
|
|
462
|
+
m.add_argument("--max-dates", type=nonnegative_int, default=0, help="cap dates for quick tests; 0 means no cap")
|
|
463
|
+
|
|
464
|
+
m.set_defaults(func=command_compare_month)
|
|
465
|
+
|
|
466
|
+
r = sub.add_parser("compare-range", help="compare a custom date range")
|
|
467
|
+
add_common(r)
|
|
468
|
+
r.add_argument("--start-date", required=True)
|
|
469
|
+
r.add_argument("--end-date", required=True)
|
|
470
|
+
r.add_argument("--step-days", type=positive_int, default=7)
|
|
471
|
+
r.add_argument("--max-dates", type=nonnegative_int, default=0)
|
|
472
|
+
|
|
473
|
+
r.set_defaults(func=command_compare_range)
|
|
474
|
+
|
|
475
|
+
y = sub.add_parser("compare-years", help="compare the same month-day across years")
|
|
476
|
+
add_common(y)
|
|
477
|
+
y.add_argument("--years", required=True, help="comma separated years, e.g. 2026,2027")
|
|
478
|
+
y.add_argument("--month-day", required=True, help="MM-DD, e.g. 06-01")
|
|
479
|
+
y.add_argument("--max-dates", type=nonnegative_int, default=0)
|
|
480
|
+
|
|
481
|
+
y.set_defaults(func=command_compare_years)
|
|
482
|
+
return p
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def main() -> None:
|
|
486
|
+
parser = build_parser()
|
|
487
|
+
args = parser.parse_args()
|
|
488
|
+
preflight_validate_args(args)
|
|
489
|
+
|
|
490
|
+
ensure_runtime()
|
|
491
|
+
if getattr(args, "max_dates", 0) == 0:
|
|
492
|
+
args.max_dates = None
|
|
493
|
+
payload = args.func(args)
|
|
494
|
+
if args.format == "json":
|
|
495
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
496
|
+
else:
|
|
497
|
+
print_markdown(payload)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
if __name__ == "__main__":
|
|
501
|
+
main()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "flight-ticket-search",
|
|
3
|
+
"description": "Google Flights ๊ณต๊ฐ ๊ฒ์ ํ๋ฉด์ ๋ฌด๋ฃ๋ก ์กฐํํด ํญ๊ณต๊ถ ํ๋ณด, ์์ฝ ๊ฒ์ ๋งํฌ, ๋ ์ง/์/์ฐ๋๋ณ ์ต์ ๊ฐยทํ๊ท ๊ฐ ๋น๊ต๋ฅผ ๋ณด์์ ์ผ๋ก ์ ๊ณตํ๋ค. ๋์ ์์๋ ๊ณต์ ํ๋ฉด์ ํตํ ํ์ ์ก์
๊น์ง ์งํํ๋ค.",
|
|
4
|
+
"profiles": [
|
|
5
|
+
"browser",
|
|
6
|
+
"action:booking"
|
|
7
|
+
],
|
|
8
|
+
"frontmatter": "name: flight-ticket-search\ndescription: Google Flights ๊ณต๊ฐ ๊ฒ์ ํ๋ฉด์ ๋ฌด๋ฃ๋ก ์กฐํํด ํญ๊ณต๊ถ ํ๋ณด, ์์ฝ ๊ฒ์ ๋งํฌ, ๋ ์ง/์/์ฐ๋๋ณ ์ต์ ๊ฐยทํ๊ท ๊ฐ ๋น๊ต๋ฅผ ๋ณด์์ ์ผ๋ก ์ ๊ณตํ๋ค. ๋์ ์์๋ ๊ณต์ ํ๋ฉด์ ํตํ ํ์ ์ก์
๊น์ง ์งํํ๋ค.\nlicense: MIT\nmetadata:\n category: travel\n locale: ko-KR\n phase: v1"
|
|
9
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Foresttrip Vacancy
|
|
2
|
+
|
|
3
|
+
## What this skill does
|
|
4
|
+
|
|
5
|
+
์ฒ๋๋คe ๊ณต์ ์ฌ์ดํธ(`https://foresttrip.go.kr/index.jsp`)์์ ์์ฐํด์๋ฆผ ์์ฝ ๊ฐ๋ฅ ๊ฐ์ค์ ๋ ์ง ๊ธฐ์ค์ผ๋ก ์กฐํ ์๋ํํ๋ค.
|
|
6
|
+
|
|
7
|
+
generic runtime์์ helper๋ ์กฐํ ์ ์ฉ์ด๋ค. ๋์ ์์๋ ์กฐํ ๊ฒฐ๊ณผ๋ฅผ ๊ณต์ CloakBrowser ์์ฝ ํ๋ฆ์ผ๋ก ์ด์ด๊ฐ๋ฉฐ, CAPTCHAยท๋๊ธฐ์ด ์ฐํยท๊ณต๊ฒฉ์ ๋ฐ๋ณต ์ค๋์ดํ์ ์ฌ์ ํ ํ์ง ์๋๋ค.
|
|
8
|
+
|
|
9
|
+
## When to use
|
|
10
|
+
|
|
11
|
+
- "์ด๋ฒ ์ฃผ๋ง ์์ฐํด์๋ฆผ ๋น ๊ฐ์ค ์์ด?"
|
|
12
|
+
- "์ฒ๋๋คe 2026๋
5์ 4์ผ ์์ฝ ๊ฐ๋ฅํ ๊ณณ ์กฐํํด์ค"
|
|
13
|
+
- "์์ฐํด์๋ฆผ ๋น์๋ฆฌ ์ ์ฒด ์กฐํํด์ค"
|
|
14
|
+
- "๊ด์ฌ ํด์๋ฆผ ์ค ์์ฝ ๊ฐ๋ฅํ ๊ฐ์ค๋ง ์๋ ค์ค"
|
|
15
|
+
|
|
16
|
+
## When not to use
|
|
17
|
+
|
|
18
|
+
- ๋์ ๊ฐ ์๋๋ฉฐ ์์ฝ ์ ์ฒญ์ด๋ ๊ฒฐ์ ๊น์ง ์๋ํํด์ผ ํ๋ ๊ฒฝ์ฐ
|
|
19
|
+
- ์บก์ฐจ๋ฅผ ํ๊ฑฐ๋ ๋๊ธฐ์ด์ ์ฐํํด์ผ ํ๋ ๊ฒฝ์ฐ
|
|
20
|
+
- ๊ณ์ ์ ๋ณด๋ฅผ ์ฑํ
์ฐฝ์ ์ง์ ๋ฃ์ผ๋ ค๋ ๊ฒฝ์ฐ
|
|
21
|
+
- aggressive polling, ์ค๋์ดํ, ๋ฐ๋ณต ์์ฝ ์๋๊ฐ ํ์ํ ๊ฒฝ์ฐ
|
|
22
|
+
|
|
23
|
+
## Prerequisites
|
|
24
|
+
|
|
25
|
+
- Python 3.9+
|
|
26
|
+
- Playwright Chromium browser
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
python3 -m pip install playwright
|
|
30
|
+
python3 -m playwright install chromium
|
|
31
|
+
python3 scripts/run_foresttrip_vacancy.py --check-deps
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Required environment variables
|
|
35
|
+
|
|
36
|
+
- `KSKILL_FORESTTRIP_ID`
|
|
37
|
+
- `KSKILL_FORESTTRIP_PASSWORD`
|
|
38
|
+
|
|
39
|
+
Optional:
|
|
40
|
+
|
|
41
|
+
- none
|
|
42
|
+
|
|
43
|
+
### Credential handling
|
|
44
|
+
|
|
45
|
+
- ๋์ credential mode์์๋ `vault-run` capability๋ฅผ ์ฌ์ฉํ๊ณ , ์์ผ๋ฉด `request_vault_credential`์ ํธ์ถํ๋ค. ID/PW ์๋ฌธ์ ์ฑํ
์ด๋ shell์ ๋ฃ์ง ์๋๋ค.
|
|
46
|
+
- ๊ทธ ๋ฐ์ ํ๊ฒฝ์์๋ ์ด๋ฏธ ์ฃผ์
๋ ํ๊ฒฝ๋ณ์ โ host vault โ `~/.config/k-skill/secrets.env` ์์๋ก ์ฌ์ฉํ๋ค.
|
|
47
|
+
- Generic helper ์์ฒด๋ `KSKILL_FORESTTRIP_ID`, `KSKILL_FORESTTRIP_PASSWORD` ํ๊ฒฝ๋ณ์๋ง ์ฝ๋๋ค.
|
|
48
|
+
|
|
49
|
+
## Inputs
|
|
50
|
+
|
|
51
|
+
- ๋ ์ง: `YYYYMMDD`, ์ฌ๋ฌ ๋ ์ง๋ฉด comma-separated `YYYYMMDD,YYYYMMDD`
|
|
52
|
+
- ์กฐํ ๋ฒ์:
|
|
53
|
+
- `--all`: ์ ์ฒด ์์ฐํด์๋ฆผ ์กฐํ
|
|
54
|
+
- `--forest-id`: ํน์ `insttId` ์กฐํ
|
|
55
|
+
- `--forest-name`: ๊ณต์ ํด์๋ฆผ๋ช
๋ถ๋ถ ์ผ์น ์กฐํ
|
|
56
|
+
- ์ถ๋ ฅ ํ์:
|
|
57
|
+
- `--text`: ์ฌ๋์ฉ ์์ฝ
|
|
58
|
+
- `--json`: ๊ตฌ์กฐํ ๊ฒฐ๊ณผ
|
|
59
|
+
- ์ ํ ํํฐ:
|
|
60
|
+
- `--categories 01`: ์๋ฐ
|
|
61
|
+
- `--categories 02`: ์ผ์/์บ ํ
|
|
62
|
+
- `--categories 01,02`: ์๋ฐ + ์ผ์/์บ ํ
|
|
63
|
+
- ๊ณ ๊ธ ์คํ ์ต์
:
|
|
64
|
+
- `--week-range N`: `--dates` ๋ฅผ ์๋ตํ์ ๋๋ง ์ค๋๋ถํฐ N์ฃผ ๋ฒ์๋ฅผ ์กฐํ
|
|
65
|
+
- `--concurrency N`: ๋ณ๋ ฌ ์กฐํ worker ์, 1-5 ๋ฒ์
|
|
66
|
+
- `--session-cache PATH`: ๋ก๊ทธ์ธ ์ธ์
์บ์ ๊ฒฝ๋ก override
|
|
67
|
+
|
|
68
|
+
## Workflow
|
|
69
|
+
|
|
70
|
+
### 1. Ensure credentials are available
|
|
71
|
+
|
|
72
|
+
๋์ credential mode์์๋ ์ฒ๋๋คe capability๋ฅผ ์ฌ์ฉํ๊ณ , ์์ผ๋ฉด `request_vault_credential`์ ํธ์ถํ๋ค. generic fallback์์๋ง `KSKILL_FORESTTRIP_ID`, `KSKILL_FORESTTRIP_PASSWORD`๋ฅผ ํ์ธํ๋ค.
|
|
73
|
+
|
|
74
|
+
์ํฌ๋ฆฟ์ด ์๋ค๋ ์ด์ ๋ก ๋์ฒด ์ฌ์ดํธ, ์บก์ฐจ ์ฐํ, ๋น๊ณต์ ์์ฝ ๊ฒฝ๋ก๋ฅผ ์ฐพ์ง ์๋๋ค.
|
|
75
|
+
|
|
76
|
+
### 2. Install runtime dependencies when missing
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
python3 -m pip install playwright
|
|
80
|
+
python3 -m playwright install chromium
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 3. Run a vacancy lookup
|
|
84
|
+
|
|
85
|
+
์ด ์คํฌ์ helper๋ฅผ ํตํด ์กฐํํ๋ค. Helper๋ Playwright๋ก ์ฒ๋๋คe์ ๋ก๊ทธ์ธํด CSRF/cookie์ ๊ณต์ ํด์๋ฆผ ID ๋ชฉ๋ก์ ์ป์ ๋ค, ์๋ณ์์ฝ์กฐํ JSON endpoint๋ง ํธ์ถํ๋ค.
|
|
86
|
+
|
|
87
|
+
2026-04-29 ํ์ธ ๊ธฐ์ค, ๋ก๊ทธ์ธ ์์ด ์๋ณ์์ฝ์กฐํ ํ๋ฉด์ ์ ๊ทผํ๋ฉด `401 Unauthorized`๊ฐ ๋ฐํ๋๊ณ , ์กฐํ endpoint๋ JSON ๋์ ์๋ด HTML์ ๋ฐํํ๋ค. ๋ฐ๋ผ์ ํ์ฌ helper๋ ๋ก๊ทธ์ธ ์ธ์
/CSRF ํ๋ณด๋ฅผ ํ์ ์ ์ ๋ก ๋๋ค.
|
|
88
|
+
|
|
89
|
+
API๋ `srchDate` ๋จ์ผ ์ผ์๋ง ์์ฒญํด๋ ์๋ต์ 5์ผ ์๋์ฐ๋ฅผ ํฌํจํ ์ ์๋ค. helper๋ ์์ฒญ ๋ฒ์(`today`โ`last_day`) ๋ฐ `useDt` ํ์ ์๋ ์ ๊ฑฐํ๋ฏ๋ก ์ฌ์ฉ์์๊ฒ๋ ์์ฒญํ ๋ ์ง์ ๋น์๋ฆฌ๋ง ๋
ธ์ถ๋๋ค.
|
|
90
|
+
|
|
91
|
+
์ ์ฒด ์์ฐํด์๋ฆผ์์ ํน์ ๋ ์ง ์กฐํ:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
python3 scripts/run_foresttrip_vacancy.py --all --text --dates 20260504
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
JSON ์ถ๋ ฅ:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
python3 scripts/run_foresttrip_vacancy.py --all --json --dates 20260504
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
์บ ํ/์ผ์๋ง ์กฐํ:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
python3 scripts/run_foresttrip_vacancy.py --all --text --dates 20260504 --categories 02
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
ํน์ ํด์๋ฆผ๋ช
์ผ๋ก ์กฐํ:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
python3 scripts/run_foresttrip_vacancy.py --forest-name ์ ๋ช
์ฐ --text --dates 20260504
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 4. Summarize results conservatively
|
|
116
|
+
|
|
117
|
+
์๋ต์ ์๋ ํญ๋ชฉ ์ค์ฌ์ผ๋ก ์งง๊ฒ ์ ๋ฆฌํ๋ค.
|
|
118
|
+
|
|
119
|
+
- ์กฐํ ๋ ์ง
|
|
120
|
+
- ์กฐํ ๋ฒ์
|
|
121
|
+
- ์์ฝ ๊ฐ๋ฅํ ํด์๋ฆผ๋ช
|
|
122
|
+
- ๊ฐ์ค/์์ค๋ช
|
|
123
|
+
- ์๋ฐ/์ผ์ ๊ตฌ๋ถ
|
|
124
|
+
- ์ ์ ๋๋ ์์ฉ ์ธ์
|
|
125
|
+
- fetch failure๊ฐ ์์ผ๋ฉด ์คํจ ๊ฐ์
|
|
126
|
+
|
|
127
|
+
๊ฒฐ๊ณผ๊ฐ ์์ผ๋ฉด "์กฐํ ์์ ๊ธฐ์ค ์์ฝ ๊ฐ๋ฅ ๊ฐ์ค ์์"์ด๋ผ๊ณ ๋งํ๋ค. ์ค์ ์์ฝ ๊ฐ๋ฅ ์ฌ๋ถ๋ ์ฒ๋๋คe ํ๋ฉด์์ ์ฌํ์ธ๋ ์ ์์์ ๋ง๋ถ์ธ๋ค.
|
|
128
|
+
|
|
129
|
+
`goodsNm`์ "์๋น"๊ฐ ํฌํจ๋ ๊ฐ์ค์ ์ด์์๊ฐ ๋ณด์ ํ๋ ๋ด๋ถ์ฉ ์๋ฆฌ๋ก, ์ฌ์ฉ์ ์์ฝ ํ๋ฉด์๋ ๋
ธ์ถ๋์ง ์๋๋ค. helper๋ ์ด ๊ฐ์ค๋ค์ ๊ฒฐ๊ณผ์์ ์๋ ์ ์ธํ๋ค. ๊ฐ์ `(ํด์๋ฆผ, ๋ ์ง, ๊ฐ์ค๋ช
)` ์กฐํฉ์ ์ค๋ณต ํ๋ dedup๋๋ค.
|
|
130
|
+
|
|
131
|
+
## Done when
|
|
132
|
+
|
|
133
|
+
- ์์ฒญ ๋ ์ง์ ์กฐํ ๋ฒ์๊ฐ ๋ช
ํํ๋ค.
|
|
134
|
+
- read-only ์๋ณ์์ฝ์กฐํ helper๋ฅผ ์ต์ 1ํ ์คํํ๋ค.
|
|
135
|
+
- ๋น ๊ฐ์ค์ด ์์ผ๋ฉด ๋ ์ง/ํด์๋ฆผ/๊ฐ์ค์ ์ ๋ฆฌํ๋ค.
|
|
136
|
+
- ๋น ๊ฐ์ค์ด ์์ผ๋ฉด ์๋ค๊ณ ๋ช
ํํ ๋งํ๋ค.
|
|
137
|
+
- ๋์ ์ ์์ฝ ์์ฒญ์ด๋ฉด ๊ณต์ ์์ฝ ํ๋ฆ์ ์งํํ๊ณ , ๊ฒฐ์ ๊ฐ ํ์ํ๋ฉด `clarify` ์น์ธ ํ ์๋ฃ ์ํ๋ฅผ ํ์ธํ๋ค.
|
|
138
|
+
- CAPTCHA/๋๊ธฐ์ด ์ฐํ๋ ์๋ํ์ง ์์๋ค.
|
|
139
|
+
|
|
140
|
+
## Failure modes
|
|
141
|
+
|
|
142
|
+
- ๋ก๊ทธ์ธ ์คํจ: `KSKILL_FORESTTRIP_ID`, `KSKILL_FORESTTRIP_PASSWORD` ํ์ธ
|
|
143
|
+
- Playwright browser ๋ฏธ์ค์น: `python3 -m playwright install chromium`
|
|
144
|
+
- fetch failure ์ผ๋ถ ๋ฐ์: ๊ฒฐ๊ณผ์ ์คํจ ๊ฐ์๋ฅผ ํจ๊ป ๋ณด๊ณ ํ๊ณ , ํ์ํ๋ฉด `--refresh-session` ์ผ๋ก 1ํ ์ฌ์กฐํ
|
|
145
|
+
- ์ฒ๋๋คe ํ๋ฉด ๋ณ๊ฒฝ: helper์ login/session bootstrap ๋๋ parser ์ ๊ฒ ํ์
|
|
146
|
+
- "(์๋น)" ๊ฐ์ค์ด ๊ฒฐ๊ณผ์ ์ ๋์ด: ์ ์ ๋์์ด๋ค. ์ฌ์ฉ์ ์์ฝ ํ๋ฉด์ ๋
ธ์ถ๋์ง ์๋ ์ด์์ ๋ณด์ ๋ถ์ด๋ผ ์๋์ ์ผ๋ก ์ ์ธ๋๋ค.
|
|
147
|
+
- ์ฌ์ฉ์ ํ๋ฉด ๊ฐ์ค ์์ helper ๊ฒฐ๊ณผ๊ฐ ๋ค๋ฆ: ๊ฐ์ ๊ฐ์ค์ ์ค๋ณต ํ์ด dedup๋์๊ฑฐ๋, ์์ฒญ ๋ฒ์ ๋ฐ `useDt`๊ฐ ์ ๊ฑฐ๋์ ๊ฐ๋ฅ์ฑ์ด ๋๋ค. raw API ์๋ต์ ํ์ธํ๋ ค๋ฉด helper ๋ก์ง์ ์ฐํํด์ ์ง์ ํธ์ถ ํ์.
|
|
148
|
+
|
|
149
|
+
## Maintainer review notes
|
|
150
|
+
|
|
151
|
+
๋ฉ์ธํ
์ด๋๊ฐ ์ด ์คํฌ์ ๊ฒํ ํ๊ธฐ ์ํด ์ฒ๋๋คe ๊ณ์ ์ ์๋ก ๋ง๋ค ํ์๋ ์๋ค.
|
|
152
|
+
|
|
153
|
+
๊ณ์ ์์ด ๊ฐ๋ฅํ ๊ฒ์ฆ:
|
|
154
|
+
|
|
155
|
+
- `./scripts/validate-skills.sh`
|
|
156
|
+
- `python3 -m py_compile foresttrip-vacancy/scripts/run_foresttrip_vacancy.py`
|
|
157
|
+
- `python3 foresttrip-vacancy/scripts/run_foresttrip_vacancy.py --help`
|
|
158
|
+
- `python3 foresttrip-vacancy/scripts/run_foresttrip_vacancy.py --check-deps`
|
|
159
|
+
- `npm run ci`
|
|
160
|
+
|
|
161
|
+
์ค์ live smoke๋ ๊ธฐ์ฌ์ ๋๋ ์ด๋ฏธ ์ฒ๋๋คe ๊ณ์ ์ ๊ฐ์ง ์ฌ์ฉ์๊ฐ ์ ํ์ ์ผ๋ก ์ํํ๋ค. PR์๋ `forests_scanned`, `fetch_failures`, `filter_hits` ๊ฐ์ ๋น๋ฏผ๊ฐ ์์ฝ๋ง ๋จ๊ธฐ๊ณ ๊ณ์ ์ ๋ณด, ์ธ์
์ฟ ํค, ๊ฐ์ธ ์กฐํ ์ธ๋ถ ๋ด์ญ์ ๊ณต์ ํ์ง ์๋๋ค.
|
|
162
|
+
|
|
163
|
+
## Safety notes
|
|
164
|
+
|
|
165
|
+
- generic helper๋ ์กฐํ ์ ์ฉ์ด๊ณ , ๋์ ์ ์์ฝ/๊ฒฐ์ ๋ CloakBrowser action path๋ก ๋ถ๋ฆฌํ๋ค.
|
|
166
|
+
- ์บก์ฐจ ์ฒ๋ฆฌ, ๋๊ธฐ์ด ์ฐํ, ๊ณต๊ฒฉ์ ์ธ ๋ฐ๋ณต ์กฐํ๋ฅผ ํ์ง ์๋๋ค.
|
|
167
|
+
- ๋์ ์์๋ vault action์ ์ฌ์ฉํ๊ณ , generic fallback์์๋ง ํ๊ฒฝ๋ณ์ ๋๋ `~/.config/k-skill/secrets.env`๋ฅผ ์ฌ์ฉํ๋ค.
|