@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.
Files changed (350) hide show
  1. package/bin/k-skill.js +60 -0
  2. package/package.json +37 -0
  3. package/skills/assembly-bill-vote-search/instruction.md +111 -0
  4. package/skills/assembly-bill-vote-search/skill.json +10 -0
  5. package/skills/biz-health-check/instruction.md +69 -0
  6. package/skills/biz-health-check/scripts/biz_health_check.py +163 -0
  7. package/skills/biz-health-check/skill.json +10 -0
  8. package/skills/bok-ecos-stats/instruction.md +95 -0
  9. package/skills/bok-ecos-stats/scripts/bok_ecos.py +302 -0
  10. package/skills/bok-ecos-stats/skill.json +10 -0
  11. package/skills/building-register-search/instruction.md +69 -0
  12. package/skills/building-register-search/scripts/building_register.py +309 -0
  13. package/skills/building-register-search/scripts/building_register_xml.py +32 -0
  14. package/skills/building-register-search/skill.json +9 -0
  15. package/skills/bunjang-search/instruction.md +154 -0
  16. package/skills/bunjang-search/skill.json +10 -0
  17. package/skills/catchtable-sniper/instruction.md +270 -0
  18. package/skills/catchtable-sniper/skill.json +9 -0
  19. package/skills/cheap-gas-nearby/instruction.md +107 -0
  20. package/skills/cheap-gas-nearby/skill.json +10 -0
  21. package/skills/corporate-registration-consulting/instruction.md +117 -0
  22. package/skills/corporate-registration-consulting/scripts/fill_official_hwp.py +106 -0
  23. package/skills/corporate-registration-consulting/skill.json +8 -0
  24. package/skills/coupang-product-search/instruction.md +219 -0
  25. package/skills/coupang-product-search/scripts/coupang_partners_mcp.py +146 -0
  26. package/skills/coupang-product-search/skill.json +10 -0
  27. package/skills/court-auction-notice-search/instruction.md +201 -0
  28. package/skills/court-auction-notice-search/skill.json +9 -0
  29. package/skills/court-payment-order-assistant/instruction.md +120 -0
  30. package/skills/court-payment-order-assistant/skill.json +10 -0
  31. package/skills/d2b-notice-search/instruction.md +117 -0
  32. package/skills/d2b-notice-search/skill.json +10 -0
  33. package/skills/daangn-cars-search/instruction.md +91 -0
  34. package/skills/daangn-cars-search/scripts/daangn_cars.py +72 -0
  35. package/skills/daangn-cars-search/skill.json +9 -0
  36. package/skills/daangn-jobs-search/instruction.md +90 -0
  37. package/skills/daangn-jobs-search/scripts/daangn_jobs.py +98 -0
  38. package/skills/daangn-jobs-search/skill.json +9 -0
  39. package/skills/daangn-realty-search/instruction.md +114 -0
  40. package/skills/daangn-realty-search/scripts/daangn_detail_ld.py +76 -0
  41. package/skills/daangn-realty-search/scripts/daangn_realty.py +216 -0
  42. package/skills/daangn-realty-search/scripts/daangn_relay_store.py +174 -0
  43. package/skills/daangn-realty-search/skill.json +8 -0
  44. package/skills/daangn-used-goods-search/instruction.md +90 -0
  45. package/skills/daangn-used-goods-search/scripts/daangn_used_goods.py +80 -0
  46. package/skills/daangn-used-goods-search/skill.json +9 -0
  47. package/skills/daishin-report-search/instruction.md +138 -0
  48. package/skills/daishin-report-search/skill.json +10 -0
  49. package/skills/daiso-product-search/instruction.md +172 -0
  50. package/skills/daiso-product-search/skill.json +8 -0
  51. package/skills/danawa-price-search/instruction.md +183 -0
  52. package/skills/danawa-price-search/scripts/danawa_search.py +354 -0
  53. package/skills/danawa-price-search/skill.json +8 -0
  54. package/skills/delivery-tracking/instruction.md +358 -0
  55. package/skills/delivery-tracking/skill.json +8 -0
  56. package/skills/donation-place-search/instruction.md +129 -0
  57. package/skills/donation-place-search/skill.json +9 -0
  58. package/skills/emergency-room-beds/instruction.md +82 -0
  59. package/skills/emergency-room-beds/skill.json +8 -0
  60. package/skills/ev-charger-nearby/instruction.md +80 -0
  61. package/skills/ev-charger-nearby/scripts/ev_charger.py +222 -0
  62. package/skills/ev-charger-nearby/skill.json +10 -0
  63. package/skills/ev-subsidy-status/instruction.md +165 -0
  64. package/skills/ev-subsidy-status/skill.json +10 -0
  65. package/skills/express-bus-booking/instruction.md +207 -0
  66. package/skills/express-bus-booking/references/kobus-http-flow.md +159 -0
  67. package/skills/express-bus-booking/scripts/kobus_express_booking.py +243 -0
  68. package/skills/express-bus-booking/skill.json +8 -0
  69. package/skills/fine-dust-location/instruction.md +89 -0
  70. package/skills/fine-dust-location/skill.json +9 -0
  71. package/skills/flight-ticket-search/instruction.md +237 -0
  72. package/skills/flight-ticket-search/scripts/flight_ticket_search.py +501 -0
  73. package/skills/flight-ticket-search/skill.json +9 -0
  74. package/skills/foresttrip-vacancy/instruction.md +167 -0
  75. package/skills/foresttrip-vacancy/scripts/run_foresttrip_vacancy.py +549 -0
  76. package/skills/foresttrip-vacancy/skill.json +10 -0
  77. package/skills/fsc-corporate-info/instruction.md +57 -0
  78. package/skills/fsc-corporate-info/scripts/fsc_corporate_info.py +113 -0
  79. package/skills/fsc-corporate-info/skill.json +10 -0
  80. package/skills/g2b-order-plan-search/instruction.md +131 -0
  81. package/skills/g2b-order-plan-search/scripts/g2b_order_plan.py +158 -0
  82. package/skills/g2b-order-plan-search/skill.json +10 -0
  83. package/skills/g2b-sanctioned-supplier/instruction.md +61 -0
  84. package/skills/g2b-sanctioned-supplier/scripts/g2b_sanctioned_supplier.py +114 -0
  85. package/skills/g2b-sanctioned-supplier/skill.json +10 -0
  86. package/skills/gangnamunni-clinic-search/instruction.md +113 -0
  87. package/skills/gangnamunni-clinic-search/skill.json +9 -0
  88. package/skills/geeknews-search/instruction.md +69 -0
  89. package/skills/geeknews-search/scripts/geeknews_search.py +296 -0
  90. package/skills/geeknews-search/skill.json +8 -0
  91. package/skills/gongsijiga-search/instruction.md +128 -0
  92. package/skills/gongsijiga-search/skill.json +9 -0
  93. package/skills/gov-overseas-trip-report/instruction.md +488 -0
  94. package/skills/gov-overseas-trip-report/scripts/gov_overseas_trip_report.py +1206 -0
  95. package/skills/gov-overseas-trip-report/skill.json +9 -0
  96. package/skills/han-river-water-level/instruction.md +83 -0
  97. package/skills/han-river-water-level/skill.json +10 -0
  98. package/skills/highway-traffic-status/instruction.md +88 -0
  99. package/skills/highway-traffic-status/scripts/highway_traffic.py +315 -0
  100. package/skills/highway-traffic-status/skill.json +11 -0
  101. package/skills/hipass-receipt/instruction.md +97 -0
  102. package/skills/hipass-receipt/skill.json +10 -0
  103. package/skills/hola-poke-yeoksam/instruction.md +247 -0
  104. package/skills/hola-poke-yeoksam/skill.json +8 -0
  105. package/skills/household-waste-info/instruction.md +117 -0
  106. package/skills/household-waste-info/skill.json +10 -0
  107. package/skills/housing-official-price/instruction.md +177 -0
  108. package/skills/housing-official-price/skill.json +10 -0
  109. package/skills/hwp/instruction.md +206 -0
  110. package/skills/hwp/skill.json +8 -0
  111. package/skills/intercity-bus-booking/instruction.md +189 -0
  112. package/skills/intercity-bus-booking/references/tmoney-intercity-http-flow.md +126 -0
  113. package/skills/intercity-bus-booking/scripts/intercity_bus_search.py +381 -0
  114. package/skills/intercity-bus-booking/skill.json +8 -0
  115. package/skills/iros-registry-automation/instruction.md +229 -0
  116. package/skills/iros-registry-automation/scripts/iros_pdf_summary.py +249 -0
  117. package/skills/iros-registry-automation/scripts/upstream.pin +1 -0
  118. package/skills/iros-registry-automation/skill.json +9 -0
  119. package/skills/job-posting-match/instruction.md +130 -0
  120. package/skills/job-posting-match/scripts/job_posting_match.py +396 -0
  121. package/skills/job-posting-match/scripts/test_job_posting_match.py +54 -0
  122. package/skills/job-posting-match/skill.json +8 -0
  123. package/skills/jobkorea-talent-search/instruction.md +118 -0
  124. package/skills/jobkorea-talent-search/scripts/jobkorea_talent_models.py +27 -0
  125. package/skills/jobkorea-talent-search/scripts/jobkorea_talent_parse.py +186 -0
  126. package/skills/jobkorea-talent-search/scripts/jobkorea_talent_search.py +94 -0
  127. package/skills/jobkorea-talent-search/scripts/jobkorea_talent_search_condition.py +136 -0
  128. package/skills/jobkorea-talent-search/scripts/test_jobkorea_talent_search.py +76 -0
  129. package/skills/jobkorea-talent-search/skill.json +9 -0
  130. package/skills/joseon-sillok-search/instruction.md +76 -0
  131. package/skills/joseon-sillok-search/scripts/sillok_search.py +552 -0
  132. package/skills/joseon-sillok-search/skill.json +8 -0
  133. package/skills/k-dart/instruction.md +406 -0
  134. package/skills/k-dart/skill.json +8 -0
  135. package/skills/k-schoollunch-menu/instruction.md +109 -0
  136. package/skills/k-schoollunch-menu/skill.json +9 -0
  137. package/skills/k-skill-cleaner/instruction.md +80 -0
  138. package/skills/k-skill-cleaner/scripts/k_skill_cleaner.py +410 -0
  139. package/skills/k-skill-cleaner/skill.json +8 -0
  140. package/skills/k-skill-setup/instruction.md +253 -0
  141. package/skills/k-skill-setup/skill.json +11 -0
  142. package/skills/kakao-bar-nearby/instruction.md +76 -0
  143. package/skills/kakao-bar-nearby/skill.json +8 -0
  144. package/skills/kakao-map/instruction.md +176 -0
  145. package/skills/kakao-map/skill.json +10 -0
  146. package/skills/kakaotalk-mac/instruction.md +189 -0
  147. package/skills/kakaotalk-mac/skill.json +8 -0
  148. package/skills/kbl-results/instruction.md +89 -0
  149. package/skills/kbl-results/skill.json +9 -0
  150. package/skills/kbo-results/instruction.md +82 -0
  151. package/skills/kbo-results/skill.json +8 -0
  152. package/skills/keris-academic-search/instruction.md +81 -0
  153. package/skills/keris-academic-search/scripts/keris_academic.py +210 -0
  154. package/skills/keris-academic-search/skill.json +10 -0
  155. package/skills/kleague-results/instruction.md +92 -0
  156. package/skills/kleague-results/skill.json +8 -0
  157. package/skills/kopis-performance-search/instruction.md +109 -0
  158. package/skills/kopis-performance-search/skill.json +10 -0
  159. package/skills/korea-weather/instruction.md +93 -0
  160. package/skills/korea-weather/skill.json +10 -0
  161. package/skills/korean-character-count/instruction.md +87 -0
  162. package/skills/korean-character-count/scripts/korean_character_count.js +268 -0
  163. package/skills/korean-character-count/skill.json +8 -0
  164. package/skills/korean-cinema-search/instruction.md +177 -0
  165. package/skills/korean-cinema-search/skill.json +8 -0
  166. package/skills/korean-heritage-search/instruction.md +106 -0
  167. package/skills/korean-heritage-search/scripts/korean_heritage_search.py +321 -0
  168. package/skills/korean-heritage-search/skill.json +8 -0
  169. package/skills/korean-holiday-calendar/instruction.md +97 -0
  170. package/skills/korean-holiday-calendar/skill.json +9 -0
  171. package/skills/korean-humanizer/instruction.md +389 -0
  172. package/skills/korean-humanizer/references/ai-tell-taxonomy.md +147 -0
  173. package/skills/korean-humanizer/skill.json +8 -0
  174. package/skills/korean-jangbu-for/instruction.md +133 -0
  175. package/skills/korean-jangbu-for/scripts/install.sh +237 -0
  176. package/skills/korean-jangbu-for/scripts/upstream.pin +1 -0
  177. package/skills/korean-jangbu-for/skill.json +8 -0
  178. package/skills/korean-law-search/instruction.md +126 -0
  179. package/skills/korean-law-search/skill.json +10 -0
  180. package/skills/korean-marathon-schedule/instruction.md +111 -0
  181. package/skills/korean-marathon-schedule/skill.json +9 -0
  182. package/skills/korean-middle-korean/instruction.md +79 -0
  183. package/skills/korean-middle-korean/scripts/korean_middle_korean.js +214 -0
  184. package/skills/korean-middle-korean/skill.json +8 -0
  185. package/skills/korean-patent-search/instruction.md +79 -0
  186. package/skills/korean-patent-search/scripts/patent_search.py +409 -0
  187. package/skills/korean-patent-search/skill.json +9 -0
  188. package/skills/korean-privacy-terms/instruction.md +128 -0
  189. package/skills/korean-privacy-terms/scripts/install.sh +108 -0
  190. package/skills/korean-privacy-terms/scripts/upstream.pin +1 -0
  191. package/skills/korean-privacy-terms/skill.json +8 -0
  192. package/skills/korean-scholarship-search/instruction.md +317 -0
  193. package/skills/korean-scholarship-search/references/report-format.md +40 -0
  194. package/skills/korean-scholarship-search/references/school-discovery.md +61 -0
  195. package/skills/korean-scholarship-search/references/search-clues.md +58 -0
  196. package/skills/korean-scholarship-search/references/source-patterns.md +67 -0
  197. package/skills/korean-scholarship-search/scripts/scholarship_filter.py +811 -0
  198. package/skills/korean-scholarship-search/scripts/test_scholarship_filter.py +224 -0
  199. package/skills/korean-scholarship-search/scripts/university_search_plan.py +148 -0
  200. package/skills/korean-scholarship-search/skill.json +8 -0
  201. package/skills/korean-slang-writing/instruction.md +181 -0
  202. package/skills/korean-slang-writing/scripts/_slang_http.py +91 -0
  203. package/skills/korean-slang-writing/scripts/slang_lookup.py +291 -0
  204. package/skills/korean-slang-writing/scripts/slang_search.py +284 -0
  205. package/skills/korean-slang-writing/skill.json +8 -0
  206. package/skills/korean-spell-check/instruction.md +105 -0
  207. package/skills/korean-spell-check/scripts/korean_spell_check.py +523 -0
  208. package/skills/korean-spell-check/skill.json +9 -0
  209. package/skills/korean-stock-search/instruction.md +194 -0
  210. package/skills/korean-stock-search/skill.json +10 -0
  211. package/skills/korean-transit-route/instruction.md +113 -0
  212. package/skills/korean-transit-route/skill.json +10 -0
  213. package/skills/kosis-stats/instruction.md +232 -0
  214. package/skills/kosis-stats/references/kosis-openapi-guide.md +171 -0
  215. package/skills/kosis-stats/scripts/run_kosis_stats.py +896 -0
  216. package/skills/kosis-stats/skill.json +9 -0
  217. package/skills/kr-whois-lookup/instruction.md +107 -0
  218. package/skills/kr-whois-lookup/skill.json +10 -0
  219. package/skills/kstartup-search/instruction.md +186 -0
  220. package/skills/kstartup-search/scripts/run_kstartup.py +424 -0
  221. package/skills/kstartup-search/skill.json +11 -0
  222. package/skills/ktx-booking/instruction.md +244 -0
  223. package/skills/ktx-booking/skill.json +10 -0
  224. package/skills/lck-analytics/instruction.md +192 -0
  225. package/skills/lck-analytics/scripts/_lib.js +103 -0
  226. package/skills/lck-analytics/scripts/analyze-live-game.js +52 -0
  227. package/skills/lck-analytics/scripts/build-match-report.js +44 -0
  228. package/skills/lck-analytics/scripts/sync-oracle.js +50 -0
  229. package/skills/lck-analytics/skill.json +9 -0
  230. package/skills/lh-notice-search/instruction.md +206 -0
  231. package/skills/lh-notice-search/skill.json +10 -0
  232. package/skills/library-book-search/instruction.md +139 -0
  233. package/skills/library-book-search/skill.json +9 -0
  234. package/skills/local-election-candidate-search/instruction.md +77 -0
  235. package/skills/local-election-candidate-search/skill.json +8 -0
  236. package/skills/localdata-business-status/instruction.md +64 -0
  237. package/skills/localdata-business-status/scripts/localdata_business_status.py +206 -0
  238. package/skills/localdata-business-status/skill.json +8 -0
  239. package/skills/lotto-results/instruction.md +80 -0
  240. package/skills/lotto-results/skill.json +8 -0
  241. package/skills/lovebug-report/instruction.md +185 -0
  242. package/skills/lovebug-report/skill.json +9 -0
  243. package/skills/market-kurly-search/instruction.md +125 -0
  244. package/skills/market-kurly-search/skill.json +8 -0
  245. package/skills/mfds-drug-safety/instruction.md +86 -0
  246. package/skills/mfds-drug-safety/scripts/mfds_drug_safety.py +184 -0
  247. package/skills/mfds-drug-safety/skill.json +10 -0
  248. package/skills/mfds-food-safety/instruction.md +126 -0
  249. package/skills/mfds-food-safety/scripts/mfds_food_safety.py +281 -0
  250. package/skills/mfds-food-safety/skill.json +10 -0
  251. package/skills/myrealtrip-search/instruction.md +239 -0
  252. package/skills/myrealtrip-search/scripts/myrealtrip_mcp.py +194 -0
  253. package/skills/myrealtrip-search/scripts/test_myrealtrip_mcp.py +99 -0
  254. package/skills/myrealtrip-search/skill.json +9 -0
  255. package/skills/naming-house/instruction.md +146 -0
  256. package/skills/naming-house/skill.json +8 -0
  257. package/skills/national-pension-workplace/instruction.md +64 -0
  258. package/skills/national-pension-workplace/scripts/national_pension_workplace.py +113 -0
  259. package/skills/national-pension-workplace/skill.json +10 -0
  260. package/skills/naver-ad-performance/instruction.md +108 -0
  261. package/skills/naver-ad-performance/scripts/naver_ad_performance.py +240 -0
  262. package/skills/naver-ad-performance/skill.json +9 -0
  263. package/skills/naver-blog-research/instruction.md +128 -0
  264. package/skills/naver-blog-research/scripts/_naver_http.py +58 -0
  265. package/skills/naver-blog-research/scripts/naver_download_images.py +233 -0
  266. package/skills/naver-blog-research/scripts/naver_read.py +256 -0
  267. package/skills/naver-blog-research/scripts/naver_search.py +192 -0
  268. package/skills/naver-blog-research/skill.json +8 -0
  269. package/skills/naver-news-search/instruction.md +103 -0
  270. package/skills/naver-news-search/skill.json +10 -0
  271. package/skills/naver-shopping-search/instruction.md +94 -0
  272. package/skills/naver-shopping-search/skill.json +11 -0
  273. package/skills/nhis-care-checkup-search/instruction.md +116 -0
  274. package/skills/nhis-care-checkup-search/skill.json +10 -0
  275. package/skills/nts-business-registration/instruction.md +115 -0
  276. package/skills/nts-business-registration/scripts/nts_business_registration.py +215 -0
  277. package/skills/nts-business-registration/skill.json +10 -0
  278. package/skills/nts-tax-delinquency/instruction.md +54 -0
  279. package/skills/nts-tax-delinquency/scripts/nts_tax_delinquency.py +150 -0
  280. package/skills/nts-tax-delinquency/skill.json +8 -0
  281. package/skills/ohou-today-deal/instruction.md +182 -0
  282. package/skills/ohou-today-deal/scripts/ohou_today_deal.py +369 -0
  283. package/skills/ohou-today-deal/skill.json +9 -0
  284. package/skills/olive-young-search/instruction.md +154 -0
  285. package/skills/olive-young-search/skill.json +8 -0
  286. package/skills/parking-lot-search/instruction.md +96 -0
  287. package/skills/parking-lot-search/skill.json +9 -0
  288. package/skills/popbill/instruction.md +146 -0
  289. package/skills/popbill/scripts/popbill_cli.py +284 -0
  290. package/skills/popbill/scripts/popbill_registry.py +54 -0
  291. package/skills/popbill/scripts/popbill_safety.py +37 -0
  292. package/skills/popbill/scripts/popbill_templates.py +71 -0
  293. package/skills/popbill/skill.json +9 -0
  294. package/skills/public-restroom-nearby/instruction.md +89 -0
  295. package/skills/public-restroom-nearby/skill.json +9 -0
  296. package/skills/real-estate-search/instruction.md +172 -0
  297. package/skills/real-estate-search/skill.json +10 -0
  298. package/skills/rhwp-advanced/instruction.md +145 -0
  299. package/skills/rhwp-advanced/skill.json +8 -0
  300. package/skills/rhwp-edit/instruction.md +153 -0
  301. package/skills/rhwp-edit/skill.json +8 -0
  302. package/skills/s2b-notice-search/instruction.md +66 -0
  303. package/skills/s2b-notice-search/skill.json +9 -0
  304. package/skills/saju-fortune/instruction.md +177 -0
  305. package/skills/saju-fortune/skill.json +8 -0
  306. package/skills/saramin-talent-search/instruction.md +119 -0
  307. package/skills/saramin-talent-search/skill.json +9 -0
  308. package/skills/seoul-bike/instruction.md +83 -0
  309. package/skills/seoul-bike/scripts/seoul_bike.py +247 -0
  310. package/skills/seoul-bike/skill.json +10 -0
  311. package/skills/seoul-density/instruction.md +109 -0
  312. package/skills/seoul-density/scripts/seoul_density.py +271 -0
  313. package/skills/seoul-density/skill.json +10 -0
  314. package/skills/seoul-subway-arrival/instruction.md +85 -0
  315. package/skills/seoul-subway-arrival/skill.json +9 -0
  316. package/skills/sh-notice-search/instruction.md +150 -0
  317. package/skills/sh-notice-search/skill.json +9 -0
  318. package/skills/srt-booking/instruction.md +181 -0
  319. package/skills/srt-booking/scripts/srt_booking.py +272 -0
  320. package/skills/srt-booking/scripts/srt_seats.py +156 -0
  321. package/skills/srt-booking/skill.json +20 -0
  322. package/skills/subway-lost-property/instruction.md +93 -0
  323. package/skills/subway-lost-property/scripts/subway_lost_property.py +244 -0
  324. package/skills/subway-lost-property/skill.json +8 -0
  325. package/skills/ticket-availability/instruction.md +175 -0
  326. package/skills/ticket-availability/scripts/ticket_availability.py +430 -0
  327. package/skills/ticket-availability/skill.json +9 -0
  328. package/skills/toss-securities/instruction.md +116 -0
  329. package/skills/toss-securities/skill.json +9 -0
  330. package/skills/used-car-price-search/instruction.md +109 -0
  331. package/skills/used-car-price-search/skill.json +8 -0
  332. package/skills/yebigun-training/instruction.md +177 -0
  333. package/skills/yebigun-training/skill.json +9 -0
  334. package/skills/zipcode-search/instruction.md +159 -0
  335. package/skills/zipcode-search/scripts/zipcode_search.py +150 -0
  336. package/skills/zipcode-search/skill.json +8 -0
  337. package/src/assemble.js +134 -0
  338. package/src/detect.js +18 -0
  339. package/templates/action-account.md +4 -0
  340. package/templates/action-booking.md +5 -0
  341. package/templates/action-commerce.md +5 -0
  342. package/templates/action-communication.md +5 -0
  343. package/templates/action-submission.md +5 -0
  344. package/templates/browser.md +4 -0
  345. package/templates/core.md +5 -0
  346. package/templates/hard-boundary.md +3 -0
  347. package/templates/local.md +2 -0
  348. package/templates/lookup.md +2 -0
  349. package/templates/proxy.md +2 -0
  350. 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`๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.