@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,222 @@
1
+ #!/usr/bin/env python3
2
+ """Read-only EV charger info/status helper using stdlib only."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import os
9
+ import pathlib
10
+ import sys
11
+ import urllib.error
12
+ import urllib.parse
13
+ import urllib.request
14
+ from typing import Any, Dict, Optional
15
+
16
+
17
+ DEFAULT_PROXY_BASE_URL = "https://k-skill-proxy.nomadamas.org"
18
+ DEFAULT_SECRETS_PATH = pathlib.Path("~/.config/k-skill/secrets.env").expanduser()
19
+ UPSTREAM_BASE_URL = "https://apis.data.go.kr/B552584/EvCharger"
20
+ OPERATIONS = {"info": "getChargerInfo", "status": "getChargerStatus"}
21
+ PROXY_DOWN_MSG = "설정된 k-skill-proxy 프록시 서버가 응답하지 않습니다. 잠시 후 재시도하거나 운영자에게 문의하세요."
22
+ PROXY_NOT_CONFIGURED_MSG = "k-skill-proxy에 EV 충전소 API 키가 설정되어 있지 않습니다. 운영자에게 문의하세요."
23
+
24
+
25
+ class HelperError(RuntimeError):
26
+ pass
27
+
28
+
29
+ def load_secrets(path: pathlib.Path) -> Dict[str, str]:
30
+ values: Dict[str, str] = {}
31
+ try:
32
+ lines = path.read_text(encoding="utf-8").splitlines()
33
+ except OSError:
34
+ return values
35
+ for raw in lines:
36
+ line = raw.strip()
37
+ if not line or line.startswith("#") or "=" not in line:
38
+ continue
39
+ key, value = line.split("=", 1)
40
+ value = value.strip().strip('"').strip("'")
41
+ values[key.strip()] = value
42
+ return values
43
+
44
+
45
+ def resolve_api_key(args: argparse.Namespace) -> Optional[str]:
46
+ env_key = os.environ.get("KSKILL_EV_CHARGER_API_KEY") or os.environ.get("DATA_GO_KR_API_KEY")
47
+ if env_key and env_key.strip():
48
+ return env_key.strip()
49
+ secrets = load_secrets(pathlib.Path(args.secrets_path).expanduser())
50
+ value = secrets.get("KSKILL_EV_CHARGER_API_KEY") or secrets.get("DATA_GO_KR_API_KEY")
51
+ return value.strip() if value and value.strip() else None
52
+
53
+
54
+ def _bounded_integer(value: int, label: str, minimum: int, maximum: int) -> int:
55
+ if value < minimum or value > maximum:
56
+ raise HelperError(f"{label} 값은 {minimum}~{maximum} 범위여야 합니다.")
57
+ return value
58
+
59
+
60
+ def _validate_code(value: Optional[str], label: str, maximum: int, digits: Optional[int] = None) -> Optional[str]:
61
+ if value is None:
62
+ return None
63
+ text = value.strip()
64
+ if not text:
65
+ return None
66
+ if len(text) > maximum or (digits is not None and (len(text) != digits or not text.isdigit())):
67
+ raise HelperError(f"올바른 {label} 값을 입력하세요.")
68
+ if digits is None and not all(ch.isalnum() or ch in "_-" for ch in text):
69
+ raise HelperError(f"올바른 {label} 값을 입력하세요.")
70
+ return text
71
+
72
+
73
+ def build_query(args: argparse.Namespace) -> Dict[str, Any]:
74
+ query: Dict[str, Any] = {
75
+ "pageNo": _bounded_integer(args.page_no, "pageNo", 1, 100000),
76
+ "numOfRows": _bounded_integer(args.num_of_rows, "numOfRows", 10, 9999),
77
+ }
78
+ for key, maximum, digits in (("zcode", 2, 2), ("zscode", 5, 5), ("stat_id", 40, None), ("chger_id", 10, None)):
79
+ value = _validate_code(getattr(args, key), key.replace("_", ""), maximum, digits)
80
+ if value is not None:
81
+ query[{"stat_id": "statId", "chger_id": "chgerId"}.get(key, key)] = value
82
+ if args.command == "info" and args.location:
83
+ location = args.location.strip()
84
+ if len(location) > 100:
85
+ raise HelperError("location 값은 100자 이하여야 합니다.")
86
+ if args.direct:
87
+ raise HelperError("--direct에서는 location을 지원하지 않습니다. --zcode와 --zscode를 사용하세요.")
88
+ query["location"] = location
89
+ if args.command == "status":
90
+ if args.limit_yn:
91
+ limit_yn = args.limit_yn.strip().upper()
92
+ if limit_yn not in {"Y", "N"}:
93
+ raise HelperError("limitYn 값은 Y 또는 N이어야 합니다.")
94
+ query["limitYn"] = limit_yn
95
+ if args.period is not None:
96
+ query["period"] = _bounded_integer(args.period, "period", 1, 10)
97
+ return query
98
+
99
+
100
+ def build_url(args: argparse.Namespace, query: Dict[str, Any], api_key: Optional[str]) -> str:
101
+ if args.direct:
102
+ if not api_key:
103
+ raise HelperError(
104
+ "KSKILL_EV_CHARGER_API_KEY 또는 DATA_GO_KR_API_KEY가 없습니다. "
105
+ "공공데이터포털 데이터셋 15076352 활용신청 후 키를 환경변수나 ~/.config/k-skill/secrets.env에 설정하세요."
106
+ )
107
+ direct_query = dict(query)
108
+ direct_query["serviceKey"] = api_key
109
+ direct_query["dataType"] = "JSON"
110
+ return f"{UPSTREAM_BASE_URL}/{OPERATIONS[args.command]}?{urllib.parse.urlencode(direct_query)}"
111
+ return f"{args.proxy_base_url.rstrip('/')}/v1/ev-charger/{args.command}?{urllib.parse.urlencode(query)}"
112
+
113
+
114
+ def http_get_json(url: str, timeout: int, via_proxy: bool) -> Dict[str, Any]:
115
+ request = urllib.request.Request(url, headers={"accept": "application/json", "user-agent": "k-skill/ev-charger-nearby"})
116
+ try:
117
+ with urllib.request.urlopen(request, timeout=timeout) as response:
118
+ raw = response.read().decode("utf-8", errors="replace")
119
+ except urllib.error.HTTPError as error:
120
+ raw = error.read().decode("utf-8", errors="replace") if error.fp else ""
121
+ try:
122
+ payload = json.loads(raw)
123
+ except json.JSONDecodeError:
124
+ payload = {}
125
+ if via_proxy and error.code == 503 and payload.get("error") == "upstream_not_configured":
126
+ raise HelperError(PROXY_NOT_CONFIGURED_MSG) from error
127
+ message = payload.get("message") or f"API HTTP 오류: {error.code} {error.reason}"
128
+ raise HelperError(str(message)) from error
129
+ except urllib.error.URLError as error:
130
+ if via_proxy:
131
+ raise HelperError(f"{PROXY_DOWN_MSG} (상세: {error.reason})") from error
132
+ raise HelperError(f"상류 API 네트워크 오류: {error.reason}") from error
133
+ except TimeoutError as error:
134
+ target = "프록시 서버" if via_proxy else "상류 API"
135
+ raise HelperError(f"{target} 요청 시간이 초과되었습니다.") from error
136
+ try:
137
+ payload = json.loads(raw)
138
+ except json.JSONDecodeError as error:
139
+ raise HelperError("API 응답이 올바른 JSON이 아닙니다.") from error
140
+ if not isinstance(payload, dict):
141
+ raise HelperError("API 응답 형식이 올바르지 않습니다.")
142
+ return payload
143
+
144
+
145
+ def format_text(payload: Dict[str, Any]) -> str:
146
+ response = payload.get("response")
147
+ if isinstance(response, dict) and isinstance(response.get("body"), dict):
148
+ payload = response["body"]
149
+ items_value = payload.get("items") or []
150
+ if isinstance(items_value, dict):
151
+ items_value = items_value.get("item") or []
152
+ items = items_value if isinstance(items_value, list) else [items_value]
153
+ total_count = payload.get("total_count", payload.get("totalCount", len(items)))
154
+ lines = [f"전기차 충전기 조회 결과: {total_count}건"]
155
+ for item in items:
156
+ name = item.get("statNm") or item.get("statId") or "이름 없음"
157
+ address = item.get("addr") or item.get("location") or "주소 없음"
158
+ charger = item.get("chgerId") or "-"
159
+ status = item.get("stat") or "상태 미상"
160
+ lines.append(f"- {name} / {address} / 충전기 {charger} / 상태 {status}")
161
+ if not items:
162
+ lines.append("조건에 맞는 충전기 정보가 없습니다.")
163
+ return "\n".join(lines)
164
+
165
+
166
+ def add_common_arguments(parser: argparse.ArgumentParser) -> None:
167
+ parser.add_argument("--zcode")
168
+ parser.add_argument("--zscode")
169
+ parser.add_argument("--stat-id")
170
+ parser.add_argument("--chger-id")
171
+ parser.add_argument("--page-no", type=int, default=1)
172
+ parser.add_argument("--num-of-rows", type=int, default=10)
173
+ parser.add_argument("--proxy-base-url", default=os.environ.get("KSKILL_PROXY_BASE_URL", DEFAULT_PROXY_BASE_URL))
174
+ parser.add_argument("--direct", action="store_true")
175
+ parser.add_argument("--secrets-path", default=str(DEFAULT_SECRETS_PATH))
176
+ parser.add_argument("--timeout", type=int, default=90)
177
+ parser.add_argument("--dry-run", action="store_true")
178
+ parser.add_argument("--json", action="store_true")
179
+
180
+
181
+ def build_parser() -> argparse.ArgumentParser:
182
+ parser = argparse.ArgumentParser(description="전기차 충전소 정보와 상태 조회")
183
+ subparsers = parser.add_subparsers(dest="command", required=True)
184
+ info = subparsers.add_parser("info", help="충전소 위치와 기본 정보 조회")
185
+ add_common_arguments(info)
186
+ info.add_argument("--location")
187
+ status = subparsers.add_parser("status", help="충전기 운영 상태 조회")
188
+ add_common_arguments(status)
189
+ status.add_argument("--limit-yn")
190
+ status.add_argument("--period", type=int)
191
+ return parser
192
+
193
+
194
+ def parse_args(argv: Optional[list[str]] = None) -> argparse.Namespace:
195
+ return build_parser().parse_args(argv)
196
+
197
+
198
+ def run(argv: Optional[list[str]] = None) -> int:
199
+ try:
200
+ args = parse_args(argv)
201
+ query = build_query(args)
202
+ api_key = resolve_api_key(args) if args.direct else None
203
+ if args.dry_run and args.direct:
204
+ url = build_url(args, query, "REDACTED")
205
+ else:
206
+ url = build_url(args, query, api_key)
207
+ if args.dry_run:
208
+ print(json.dumps({"operation": args.command, "url": url, "query": query}, ensure_ascii=False, indent=2))
209
+ return 0
210
+ payload = http_get_json(url, args.timeout, via_proxy=not args.direct)
211
+ if args.json:
212
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
213
+ else:
214
+ print(format_text(payload))
215
+ return 0
216
+ except HelperError as error:
217
+ print(str(error), file=sys.stderr)
218
+ return 1
219
+
220
+
221
+ if __name__ == "__main__":
222
+ raise SystemExit(run())
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "ev-charger-nearby",
3
+ "description": "Use when the user asks for Korean EV charging-station locations, charger types, or current charger status by region, station, or location text. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.",
4
+ "profiles": [
5
+ "proxy",
6
+ "vault",
7
+ "action:booking"
8
+ ],
9
+ "frontmatter": "name: ev-charger-nearby\ndescription: Use when the user asks for Korean EV charging-station locations, charger types, or current charger status by region, station, or location text. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.\nlicense: MIT\nmetadata:\n category: transit\n locale: ko-KR\n phase: v1"
10
+ }
@@ -0,0 +1,165 @@
1
+ # EV Subsidy Status
2
+
3
+ ## Prerequisites
4
+
5
+ - Node.js 18+
6
+ - `ev-subsidy-status` npm package
7
+
8
+ 기본 조회에는 브라우저, API 키, 로그인, 프록시가 필요하지 않다. 공식 화면 구조 변경을 진단할 때만 사용자가 실행한 Aside Browser, BrowserOS 또는 Chrome/Chromium CDP 세션을 선택적으로 사용한다.
9
+
10
+ ## Public access path
11
+
12
+ 기본 화면:
13
+
14
+ ```text
15
+ https://ev.or.kr/nportal/buySupprt/initSubsidyPaymentCheckAction.do
16
+ ```
17
+
18
+ 모델별 보조금 화면:
19
+
20
+ ```text
21
+ POST /nportal/buySupprt/psPopupLocalCarModelPrice.do
22
+ year=<year>&local_cd=<region-code>&car_type=<11|12|13>
23
+ ```
24
+
25
+ 확인된 DOM:
26
+
27
+ - 시도: `#localDo_cd`
28
+ - 시군구: `#local_cd1`
29
+ - 결과 열: 시도, 지역구분, 차종구분, 공고파일, 접수방법, 민간공고대수, 접수대수, 출고대수, 출고잔여대수, 비고
30
+
31
+ 페이지는 로그인 없이 공개되지만 `pnp4web`/`penc`가 본문을 보호한다. 기본 경로는 공개 응답에서 공식 `pnp4web.js` 문자표를 파싱하고 원격 코드를 실행하지 않은 채 HTML을 복원한다. 전국 행이 포함된 지급현황 표에서 요청 지역만 선택한다. 공개 keyless 화면이므로 `k-skill-proxy`를 사용하지 않는다.
32
+
33
+ ## Workflow
34
+
35
+ ### 1. 지역과 차종을 정규화한다
36
+
37
+ - 지역은 가능하면 `시도 + 시군구`로 받는다. 예: `경기 성남시`
38
+ - `중구`, `강서구`처럼 중복되는 이름이면 `regions`로 후보를 찾고 사용자가 시도를 고르게 한다.
39
+ - 차종 기본값은 `passenger`/`전기승용`이다.
40
+ - 지원 차종: `passenger`/`승용`, `cargo`/`화물`, `bus`/`승합`
41
+ - 연도 기본값은 Asia/Seoul 기준 현재 연도다.
42
+
43
+ ```bash
44
+ npx ev-subsidy-status regions --query 중구
45
+ ```
46
+
47
+ ### 2. 공식 지급현황을 조회한다
48
+
49
+ ```bash
50
+ npx ev-subsidy-status status \
51
+ --region "경기 성남시" \
52
+ --vehicle passenger \
53
+ --year 2026 \
54
+ --json
55
+ ```
56
+
57
+ 결과의 `transport`가 `direct-http`인지 확인한다. 기본 경로는 사용자 브라우저가 없어도 동작해야 한다.
58
+
59
+ 반환값에서 다음을 우선 확인한다.
60
+
61
+ - `status.notice_count`: 민간공고대수
62
+ - `status.application_count`: 접수대수
63
+ - `status.delivered_count`: 출고대수
64
+ - `status.delivery_remaining_count`: 출고잔여대수
65
+ - `availability.label`: 공고 상태 판정
66
+ - `status.note`: 지자체 비고 원문
67
+ - `source.fetched_at`: KST 조회 시각
68
+
69
+ 숫자 셀은 `전체 / 우선순위 / 법인·기관 / 예약 대상군 / 일반`으로 보존한다. 승용의 예약 대상군은 `taxi`, 화물은 `small_business` alias도 함께 반환한다.
70
+
71
+ ### 3. 비고를 숫자보다 우선한다
72
+
73
+ 다음 우선순위로 설명한다.
74
+
75
+ 1. `마감`, `소진`, `접수 종료` → `closed`
76
+ 2. `접수 예정`, `추경 예정`, `추가 공고 예정` → `scheduled`
77
+ 3. `접수 중`, `신청 기간`, `신청 가능` → `open`
78
+ 4. 명시 문구 없이 잔여 대수만 양수 → `unknown_with_remaining_count`
79
+ 5. 판단 근거 없음 → `unknown`
80
+
81
+ 잔여 대수가 양수여도 비고가 마감이면 신청 가능하다고 말하지 않는다.
82
+
83
+ ### 4. 모델을 지정했을 때만 원화 환산치를 조회한다
84
+
85
+ ```bash
86
+ npx ev-subsidy-status status \
87
+ --region "서울 강남구" \
88
+ --vehicle passenger \
89
+ --model "모델명" \
90
+ --json
91
+ ```
92
+
93
+ 직접 HTTP 경로에서도 공식 모델별 보조금 POST 표면을 조회한다. 입력한 이름이 여러 세부 모델과 일치하면 임의로 하나를 고르지 않고 `model_subsidy_candidates`에 모든 후보와 후보별 환산치를 반환한다. 정확한 세부 모델과 하나만 일치할 때는 `model_subsidy`와 `remaining_budget.model_equivalent_estimate_krw`도 함께 반환한다.
94
+
95
+ `remaining_budget.model_equivalent_estimate_krw`는 다음 가정의 환산치다.
96
+
97
+ ```text
98
+ 공식 출고잔여대수 × 선택 모델의 국비+지방비
99
+ ```
100
+
101
+ 이를 지자체의 정확한 예산 잔액이라고 표현하지 않는다. 모델별 금액, 구매자 추가지원, 물량 전환, 접수 후 예약·취소가 달라 실제 가용 예산과 다르다. 모델 조회가 실패해도 지역별 대수 결과는 유지하고 `model_lookup_error`와 경고를 보여준다.
102
+
103
+ ### 5. 사용자에게 보수적으로 답한다
104
+
105
+ 최종 답변에 포함한다.
106
+
107
+ - 지역, 차종, 기준년도
108
+ - 민간공고·접수·출고·출고잔여 대수
109
+ - 공고 상태와 비고의 핵심 문구
110
+ - 모델 환산치가 있으면 계산 가정
111
+ - “출고잔여대수는 실제 신청 가능 대수 및 정확한 원화 잔액과 다를 수 있음” 경고
112
+ - 공식 출처 URL과 KST 조회 시각
113
+
114
+ ## Direct HTTP behavior
115
+
116
+ - `POST initSubsidyPaymentCheckAction.do`에 `year1`, `car_type`, 전국 지역 조건을 전송한다.
117
+ - 응답의 `pnp4web.js` URL과 보호 payload를 추출한다.
118
+ - 원격 JavaScript를 `eval`/`vm`으로 실행하지 않는다.
119
+ - 스크립트에 선언된 조각 문자표만 파싱해 payload를 UTF-8 HTML로 복원한다.
120
+ - 지급현황 표의 공식 행과 비고를 파싱하고 지역 코드는 공고 다운로드 함수 인자에서 읽는다.
121
+ - 모델이 있으면 `psPopupLocalCarModelPrice.do`를 같은 연도·지역 코드·차종으로 호출하고 모델별 국비·지방비·합계를 파싱한다.
122
+ - 공식 스크립트나 표 구조가 바뀌면 `UPSTREAM_DECODE_FAILED` 또는 `DOM_CHANGED`로 중단한다.
123
+
124
+ ## Optional browser behavior
125
+
126
+ - `domcontentloaded` 후 목표 selector와 지역 행을 명시적으로 기다린다.
127
+ - `networkidle`은 pnp4web 백그라운드 요청 때문에 사용하지 않는다.
128
+ - 렌더링된 `<option>` label/value로 지역 코드를 동적으로 해석한다.
129
+ - 기존 사용자 탭과 프로필을 닫지 않는다.
130
+ - 스킬이 만든 페이지·컨텍스트만 정리하고 지원되는 client만 disconnect한다.
131
+ - 로그인, CAPTCHA, 결제, 전자서명, 최종 제출 경계를 우회하지 않는다.
132
+
133
+ 공식 화면 변경을 진단하거나 모델별 환산을 조회하기 위해 브라우저 경로를 선택할 때:
134
+
135
+ ```bash
136
+ npx ev-subsidy-status status --region "경기 성남시" --transport browser --provider aside
137
+ npx ev-subsidy-status status --region "경기 성남시" --transport browser --provider browseros
138
+ npx ev-subsidy-status status --region "경기 성남시" --transport browser --provider chrome-cdp
139
+ ```
140
+
141
+ ## Done when
142
+
143
+ - 요청 지역과 차종이 공식 표의 행과 일치한다.
144
+ - 공고·접수·출고·출고잔여 수치와 지자체 비고를 반환한다.
145
+ - 명시적 마감 문구를 양수 잔여 대수보다 우선한다.
146
+ - 정확한 원화 예산 잔액을 알 수 없으면 `exact_available=false`와 이유를 반환한다.
147
+ - 출처 URL과 KST 조회 시각을 표시한다.
148
+
149
+ ## Failure modes
150
+
151
+ - `REGION_REQUIRED`: 지역 입력이 없다.
152
+ - `REGION_AMBIGUOUS`: 같은 시군구 이름이 여러 시도에 있다.
153
+ - `REGION_NOT_FOUND`: 공식 지역 옵션에 없다.
154
+ - `YEAR_NOT_AVAILABLE`: 요청 연도가 선택 목록에 없다.
155
+ - `VEHICLE_TYPE_NOT_AVAILABLE`: 요청 차종을 지원하지 않거나 화면에서 찾지 못했다.
156
+ - `BROWSER_UNAVAILABLE`: 연결 가능한 사용자 실행 브라우저가 없다.
157
+ - `UPSTREAM_BLOCKED`: 빈 껍데기, 차단 또는 비정상 페이지다.
158
+ - `CAPTCHA_DETECTED`: CAPTCHA가 나타났다. 우회하지 않는다.
159
+ - `AUTH_REQUIRED`: 공개 화면이 로그인 흐름으로 바뀌었다. 우회하지 않는다.
160
+ - `RESULT_EMPTY`: 목표 지역의 결과 행이 없다.
161
+ - `DOM_CHANGED`: 공식 페이지의 선택자나 표 구조가 바뀌었다.
162
+ - `UPSTREAM_DECODE_FAILED`: `pnp4web` 문자표 또는 보호 payload 형식이 바뀌었다.
163
+ - `MODEL_LOOKUP_FAILED`: 모델별 보조금 표를 읽지 못했다.
164
+
165
+ 대상군 값이 음수이거나 전체와 부분합이 다르면 값을 0으로 고치지 않는다. 원본 수치와 경고를 함께 반환한다.
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "ev-subsidy-status",
3
+ "description": "한국 지자체별 전기차 구매보조금 민간공고·접수·출고·출고잔여 대수와 공고 상태를 환경부 무공해차 통합누리집에서 조회한다. 사용자가 사는 지역의 전기승용·전기화물·전기승합 보조금 잔여 현황, 접수 가능 여부, 마감·소진·추경 예정, 특정 모델 기준 보조금 환산치를 물을 때 사용한다. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.",
4
+ "profiles": [
5
+ "proxy",
6
+ "browser",
7
+ "action:submission"
8
+ ],
9
+ "frontmatter": "name: ev-subsidy-status\ndescription: 한국 지자체별 전기차 구매보조금 민간공고·접수·출고·출고잔여 대수와 공고 상태를 환경부 무공해차 통합누리집에서 조회한다. 사용자가 사는 지역의 전기승용·전기화물·전기승합 보조금 잔여 현황, 접수 가능 여부, 마감·소진·추경 예정, 특정 모델 기준 보조금 환산치를 물을 때 사용한다. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.\nlicense: MIT\nmetadata:\n category: local-info\n locale: ko-KR\n phase: v1"
10
+ }
@@ -0,0 +1,207 @@
1
+ # Express Bus Booking (KOBUS)
2
+
3
+ ## Overview
4
+
5
+ Use this skill for Korean 고속버스 / KOBUS timetable lookup and reservation assistance. The preferred workflow is HTTP/API-first: resolve routes, query timetables, inspect remaining seats and fares, and only use browser automation when endpoint discovery or an official web-only step blocks progress.
6
+
7
+ This skill is intentionally separate from 시외버스. KOBUS terminal codes, route structures, and checkout pages differ from Tmoney 시외버스. Use `intercity-bus-booking` for 시외버스/Tmoney flows.
8
+
9
+ ## When to Use
10
+
11
+ - The user asks for `고속버스 예매`, `고속버스 시간표`, `고속버스 예약`, `프리미엄 고속버스`, or `우등 고속버스`.
12
+ - The route is clearly a KOBUS 고속버스 route or the user names KOBUS/코버스/고속버스통합예매.
13
+ - The user wants assisted reservation up to seat selection, temporary hold, or a payment-entry link.
14
+
15
+ Do **not** use this for:
16
+
17
+ - 시외버스/Tmoney routes — use `intercity-bus-booking`.
18
+ - Final card submission or payment without explicit, narrowly scoped confirmation.
19
+ - Blind browser automation before trying the official HTTP flow.
20
+
21
+ ## Core Principles
22
+
23
+ - Use official KOBUS surfaces: `https://www.kobus.co.kr`.
24
+ - Keep a cookie jar and reuse referers. Stateless one-off POSTs are less reliable.
25
+ - Prefer desktop User-Agent and HTTP/1.1 during probing if the server behaves differently under HTTP/2.
26
+ - Default scope is assisted manual checkout: lookup, candidate presentation, seat-stage readiness, temporary seat hold, and official payment-entry page handoff.
27
+ - Do not submit card fields, agree to terms, or complete payment unless the user explicitly confirms that exact action.
28
+
29
+ ## Known HTTP Flow
30
+
31
+ See `references/kobus-http-flow.md` for session-proven endpoint details and parameter examples.
32
+
33
+ ### 1. Start a Session
34
+
35
+ Fetch either the main page or route/search page with cookies enabled:
36
+
37
+ ```text
38
+ GET https://www.kobus.co.kr/main.do
39
+ GET https://www.kobus.co.kr/mrs/rotinf.do
40
+ ```
41
+
42
+ Use a realistic desktop User-Agent and a cookie jar.
43
+
44
+ ### 2. Resolve Route / Terminal Candidates
45
+
46
+ ```text
47
+ POST /mrs/readRotLinInf.ajax
48
+ ```
49
+
50
+ The response is JSON and can include `rotInfList`, `tfrInfList`, `len`, and `codeYn`. Prefer code-confirmed routes over display-name matching because terminal names are not always unique.
51
+
52
+ ### 3. Query Timetable
53
+
54
+ ```text
55
+ POST /mrs/alcnSrch.do
56
+ ```
57
+
58
+ Typical fields include:
59
+
60
+ ```text
61
+ deprCd=010
62
+ arvlCd=700
63
+ pathDvs=sngl
64
+ pathStep=1
65
+ deprDtm=YYYYMMDD
66
+ busClsCd=0
67
+ rtrpChc=1
68
+ timeLinkMin=00
69
+ timeLinkMax=23
70
+ ```
71
+
72
+ Parse the returned HTML for schedule rows/cards and `fnSatsChc(...)` onclick arguments. Respect `mrsPsbYn=N` or any equivalent booking-disabled marker.
73
+
74
+ ### 4. Enter Seat / Fare Stage
75
+
76
+ ```text
77
+ POST /mrs/satschc.do
78
+ ```
79
+
80
+ Send the original search form fields plus selected values from `fnSatsChc(...)`, commonly including:
81
+
82
+ ```text
83
+ deprTime
84
+ alcnDeprTime
85
+ alcnDeprTrmlNo
86
+ alcnArvlTrmlNo
87
+ indVBusClsCd
88
+ cacmCd
89
+ dcDvsCd
90
+ prvtBbizEmpAcmtRt
91
+ chldSftySatsYn
92
+ dsprSatsYn
93
+ ```
94
+
95
+ The response should include `form#satsChcFrm` and hidden fare/seat values such as `adltFee`, `rmnSatsNum`, `totSatsNum`, and terminal/time fields.
96
+
97
+ ### 5. Temporary Seat Hold
98
+
99
+ ```text
100
+ POST /mrs/setPcpy.ajax
101
+ ```
102
+
103
+ Submit the `satsChcFrm` hidden fields plus selected seat/count fields. A successful response includes:
104
+
105
+ ```text
106
+ MSG_CD=S0000
107
+ pcpyNoAll
108
+ satsNoAll
109
+ ESTM_AMT
110
+ DC_AMT
111
+ TISSU_AMT
112
+ ```
113
+
114
+ Treat the hold as short-lived. If testing, if the user does not proceed, or if a new seat is chosen, release it explicitly.
115
+
116
+ ### 6. Release Temporary Hold
117
+
118
+ ```text
119
+ POST /mrs/cancPcpy.ajax
120
+ ```
121
+
122
+ Use the same relevant form fields plus the returned `pcpyNoAll` and `satsNoAll`. A successful response returns `MSG_CD=S0000`.
123
+
124
+ ## Helper Script
125
+
126
+ Use the bundled helper for KOBUS lookup and optional temporary holds:
127
+
128
+ ```bash
129
+ python3 express-bus-booking/scripts/kobus_express_booking.py \
130
+ --depart-code 021 \
131
+ --arrive-code 500 \
132
+ --date 20260520 \
133
+ --select-index 1 \
134
+ --hold-first-seat \
135
+ --output-dir /tmp/kobus-hold
136
+ ```
137
+
138
+ For Seoul to Gwangju, the verified KOBUS route is `센트럴시티(서울)` code `021` to `광주(유·스퀘어)` code `500`. A successful hold returns `MSG_CD=S0000`, `pcpyNoAll`, `satsNoAll`, fare amounts, and saves a local auto-submit helper for the official KOBUS payment-information page. Final card entry and payment remain manual. Cancel abandoned holds with `/mrs/cancPcpy.ajax` using the saved cancel fields.
139
+
140
+ ## Checkout-Entry Link Helper
141
+
142
+ A plain official checkout URL is not enough because KOBUS expects a POST body containing the selected schedule, seat, fare, and hold identifiers. The practical user-facing pattern is:
143
+
144
+ 1. Create the temporary hold server-side via `setPcpy.ajax`.
145
+ 2. Generate a short helper page that auto-submits a POST form to the official KOBUS checkout endpoint.
146
+ 3. Send the helper link to the user.
147
+ 4. The user completes card/payment fields manually on the official KOBUS page.
148
+
149
+ Desktop action:
150
+
151
+ ```text
152
+ https://www.kobus.co.kr/mrs/stplcfmpym.do
153
+ ```
154
+
155
+ Mobile-friendly action:
156
+
157
+ ```text
158
+ https://www.kobus.co.kr/mrs/stplcfmpym.do?keep=/mrs/pay
159
+ ```
160
+
161
+ The POST body should include the original seat form fields plus returned hold values such as `pcpyNoAll`, `satsNoAll`, `estmAmt`, `dcAmt`, `tissuAmt`, and `nonMbrsYn=Y` when using a non-member checkout flow.
162
+
163
+ ## Mobile Redirect Caveat
164
+
165
+ KOBUS common JavaScript can redirect narrow/mobile screens to `/mblIdx.do` unless `location.href` contains a mobile-allowed path fragment such as `/mrs/pay`. In testing, posting to:
166
+
167
+ ```text
168
+ /mrs/stplcfmpym.do?keep=/mrs/pay
169
+ ```
170
+
171
+ preserved the same checkout POST body while making the final browser URL contain `/mrs/pay`, avoiding the client-side mobile-main redirect condition. Use this variant for Discord/mobile users, but still verify with the user because in-app browsers may add their own quirks.
172
+
173
+ If mobile still lands on the homepage, recommend opening the helper link in a normal external browser or desktop browser. Do not keep retrying holds indefinitely; cancel stale holds.
174
+
175
+ ## Suggested Output Format
176
+
177
+ Keep candidate lists concise and actionable:
178
+
179
+ ```text
180
+ 고속버스 서울경부 → 부산 / 2026-05-09
181
+ 1. 00:30 심야우등 / 천일고속 / 잔여 10석 / 성인 47,600원
182
+ 2. 13:50 우등 / ...
183
+ ```
184
+
185
+ When a hold/checkout helper is created, state that the next step opens the official KOBUS payment page and that payment remains manual.
186
+
187
+ For Discord/mobile, provide links as normal text links, not fenced code blocks, so the user can tap them directly.
188
+
189
+ ## Common Pitfalls
190
+
191
+ 1. **Mixing KOBUS and Tmoney codes.** KOBUS terminal codes are not Tmoney 시외버스 terminal codes.
192
+ 2. **Assuming a cart exists.** KOBUS does not expose a shopping-cart style hold list in the tested web flow. The realistic UX is temporary seat hold plus checkout-entry handoff.
193
+ 3. **Ignoring hold cleanup.** Always cancel test holds or abandoned holds with `cancPcpy.ajax`.
194
+ 4. **Treating lookup success as payment permission.** Lookup, seat-stage entry, and temporary hold are not authorization to submit payment.
195
+ 5. **Mobile homepage redirects.** Use the `?keep=/mrs/pay` helper action for mobile; otherwise KOBUS JS may send the user to `/mblIdx.do`.
196
+ 6. **Browser automation too early.** Try direct HTTP first. Use browser tooling only to discover changed endpoints or verify final user-facing behavior.
197
+
198
+ ## Verification Checklist
199
+
200
+ - [ ] Route/terminal codes were resolved from KOBUS, not guessed.
201
+ - [ ] Timetable was queried with cookies and a realistic User-Agent.
202
+ - [ ] Candidate output includes date, departure/arrival terminals, time, class/operator when available, fare, and remaining seats.
203
+ - [ ] Seat-stage response contains expected hidden fields before attempting a hold.
204
+ - [ ] Temporary hold success was confirmed with `MSG_CD=S0000` before sending a checkout helper link.
205
+ - [ ] Stale/test holds were cancelled with `cancPcpy.ajax`.
206
+ - [ ] Payment/card fields were left for the user unless they explicitly confirmed otherwise.
207
+ - [ ] Mobile helper links use the `/mrs/pay` marker variant when sent to mobile-heavy platforms.